@vfrey/vfrey 0.0.1 → 1.0.0-beta.0

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 (603) hide show
  1. package/LICENSE +33 -0
  2. package/README.md +78 -3
  3. package/accounts.example.json +61 -0
  4. package/accounts.first-run.json +41 -0
  5. package/bin/vfrey.mjs +36 -0
  6. package/integration-tools/README.md +43 -0
  7. package/integration-tools/image/pillow.md +32 -0
  8. package/integration-tools/image/playwright.md +33 -0
  9. package/integration-tools/image/resvg.md +36 -0
  10. package/integration-tools/image/satori.md +36 -0
  11. package/integration-tools/image/sharp.md +38 -0
  12. package/integration-tools/video/blender.md +34 -0
  13. package/integration-tools/video/ffmpeg.md +41 -0
  14. package/integration-tools/video/manim.md +37 -0
  15. package/integration-tools/video/motion-canvas.md +37 -0
  16. package/integration-tools/video/remotion.md +50 -0
  17. package/integration-tools/video/revideo.md +32 -0
  18. package/package.json +74 -6
  19. package/skills/vfrey-orchestrator/SKILL.md +15 -0
  20. package/skills/vfrey-orchestrator/channel-video/SKILL.md +41 -0
  21. package/skills/vfrey-product/SKILL.md +63 -0
  22. package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/cook-one.svg +14 -0
  23. package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/cook-two.svg +22 -0
  24. package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/index.html +70 -0
  25. package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/cook-one.svg +14 -0
  26. package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/cook-two.svg +22 -0
  27. package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/index.tsx +49 -0
  28. package/skills/vfrey-product/examples/concurrency-vs-parallelism/script.json +15 -0
  29. package/skills/vfrey-product/examples/octopus-facts/index.html +60 -0
  30. package/skills/vfrey-product/examples/octopus-facts/octopus.svg +8 -0
  31. package/skills/vfrey-product/examples/octopus-facts/script.json +14 -0
  32. package/skills/vfrey-product/push/SKILL.md +15 -0
  33. package/skills/vfrey-product/scenes/NOTICE +5 -0
  34. package/skills/vfrey-product/scenes/blocks.md +57 -0
  35. package/skills/vfrey-product/scenes/craft.md +118 -0
  36. package/skills/vfrey-product/scenes/motion.md +88 -0
  37. package/src/analytics/dashboard.ts +109 -0
  38. package/src/analytics/index.ts +260 -0
  39. package/src/api/client.ts +282 -0
  40. package/src/api/commands.ts +330 -0
  41. package/src/api/config.ts +34 -0
  42. package/src/api/creators.ts +463 -0
  43. package/src/api/credentials.ts +152 -0
  44. package/src/api/mail.ts +333 -0
  45. package/src/api/topics.ts +207 -0
  46. package/src/api/types.ts +205 -0
  47. package/src/cli.ts +467 -0
  48. package/src/cloud/executor.ts +281 -0
  49. package/src/cloud/job.ts +106 -0
  50. package/src/cloud/render-job.ts +187 -0
  51. package/src/config.ts +309 -0
  52. package/src/dashboard-link.ts +180 -0
  53. package/src/dashboard-start.ts +103 -0
  54. package/src/desk/channel.ts +64 -0
  55. package/src/desk/cli.ts +216 -0
  56. package/src/desk/creators-route.ts +129 -0
  57. package/src/desk/fonts/JetBrainsMono-OFL.txt +93 -0
  58. package/src/desk/fonts/google-sans-flex-latin.woff2 +0 -0
  59. package/src/desk/fonts/jetbrains-mono-medium.woff2 +0 -0
  60. package/src/desk/http.ts +460 -0
  61. package/src/desk/mail-route.ts +103 -0
  62. package/src/desk/overlay.ts +112 -0
  63. package/src/desk/preview.ts +422 -0
  64. package/src/desk/screens.ts +149 -0
  65. package/src/desk/secrets.ts +143 -0
  66. package/src/desk/series.ts +127 -0
  67. package/src/desk/state.ts +77 -0
  68. package/src/desk/studio-route.ts +119 -0
  69. package/src/desk/tick-runner.ts +66 -0
  70. package/src/desk/tick.ts +56 -0
  71. package/src/desk/topics-route.ts +41 -0
  72. package/src/desk/ui/app.css +505 -0
  73. package/src/desk/ui/app.js +185 -0
  74. package/src/desk/ui/apple-icon.png +0 -0
  75. package/src/desk/ui/components.js +79 -0
  76. package/src/desk/ui/core.js +156 -0
  77. package/src/desk/ui/favicon.ico +0 -0
  78. package/src/desk/ui/icon.svg +1 -0
  79. package/src/desk/ui/icons.js +49 -0
  80. package/src/desk/ui/index.html +41 -0
  81. package/src/desk/ui/metrics-math.d.ts +22 -0
  82. package/src/desk/ui/metrics-math.js +135 -0
  83. package/src/desk/ui/theme.js +5 -0
  84. package/src/desk/ui/views-creators.js +209 -0
  85. package/src/desk/ui/views-keys.js +114 -0
  86. package/src/desk/ui/views-mail.js +188 -0
  87. package/src/desk/ui/views-misc.js +123 -0
  88. package/src/desk/ui/views-review.js +119 -0
  89. package/src/desk/ui/views-runs.js +350 -0
  90. package/src/desk/ui/views-screens.js +163 -0
  91. package/src/desk/ui/views-series.js +318 -0
  92. package/src/desk/ui/views-topics.js +87 -0
  93. package/src/desk/verbs.ts +361 -0
  94. package/src/doctor.ts +212 -0
  95. package/src/drain.ts +14 -0
  96. package/src/engage/index.ts +217 -0
  97. package/src/generate/index.ts +110 -0
  98. package/src/generate/script.ts +152 -0
  99. package/src/generate/title.ts +78 -0
  100. package/src/help.ts +233 -0
  101. package/src/home.ts +45 -0
  102. package/src/llm/client.ts +177 -0
  103. package/src/log.ts +7 -0
  104. package/src/oauth/tokens.ts +189 -0
  105. package/src/oauth/vault.ts +72 -0
  106. package/src/pipeline/review.ts +642 -0
  107. package/src/pipeline/run.ts +443 -0
  108. package/src/pipeline/series-guard.ts +26 -0
  109. package/src/pipeline/source-fetch.ts +69 -0
  110. package/src/pipeline/stages.ts +728 -0
  111. package/src/pipeline/topics-source.ts +90 -0
  112. package/src/product/commands.ts +296 -0
  113. package/src/product/config-file.ts +38 -0
  114. package/src/product/engine-choice.ts +89 -0
  115. package/src/product/engines.ts +103 -0
  116. package/src/product/insights.ts +50 -0
  117. package/src/product/keys.ts +67 -0
  118. package/src/product/paths.ts +18 -0
  119. package/src/product/probes.ts +88 -0
  120. package/src/product/push-flow.ts +90 -0
  121. package/src/product/render-video.ts +21 -0
  122. package/src/product/short.ts +62 -0
  123. package/src/product/update.ts +32 -0
  124. package/src/project/canonical.ts +14 -0
  125. package/src/project/commands.ts +177 -0
  126. package/src/project/dashboard-status.ts +28 -0
  127. package/src/project/discover.ts +19 -0
  128. package/src/project/index-file.ts +73 -0
  129. package/src/project/marker.ts +87 -0
  130. package/src/project/migrate.ts +201 -0
  131. package/src/project/names.ts +27 -0
  132. package/src/project/notice.ts +16 -0
  133. package/src/project/open.ts +88 -0
  134. package/src/project/scaffold.ts +112 -0
  135. package/src/project/templates.ts +42 -0
  136. package/src/project/videos.ts +64 -0
  137. package/src/publish/facebook.ts +91 -0
  138. package/src/publish/index.ts +12 -0
  139. package/src/publish/instagram.ts +97 -0
  140. package/src/publish/rate.ts +62 -0
  141. package/src/publish/session.ts +242 -0
  142. package/src/publish/tiktok.ts +202 -0
  143. package/src/publish/youtube.ts +177 -0
  144. package/src/queue/desk-ops.ts +1170 -0
  145. package/src/queue/seal.ts +3 -0
  146. package/src/queue/store.ts +1006 -0
  147. package/src/remotion/AiLabel.tsx +26 -0
  148. package/src/remotion/Root.tsx +73 -0
  149. package/src/remotion/defaults.ts +26 -0
  150. package/src/remotion/films/claude/ClaudeFilm.tsx +1014 -0
  151. package/src/remotion/films/engine/EngineMark.tsx +17 -0
  152. package/src/remotion/films/rea/ReaShort.tsx +552 -0
  153. package/src/remotion/films/rea/drawings.tsx +907 -0
  154. package/src/remotion/films/rea/timing.ts +144 -0
  155. package/src/remotion/index.ts +4 -0
  156. package/src/remotion/templates/EditorialSplit.tsx +39 -0
  157. package/src/remotion/templates/KineticType.tsx +39 -0
  158. package/src/remotion/templates/SignalBoard.tsx +50 -0
  159. package/src/remotion/timing.ts +22 -0
  160. package/src/render/chrome.ts +78 -0
  161. package/src/render/engine-mark.ts +29 -0
  162. package/src/render/framey-template.ts +130 -0
  163. package/src/render/html-engine.ts +149 -0
  164. package/src/render/node-crypto-stub.ts +11 -0
  165. package/src/render/probe.ts +29 -0
  166. package/src/render/props.ts +17 -0
  167. package/src/render/vertical-short.ts +35 -0
  168. package/src/render/webpack.ts +33 -0
  169. package/src/render/worker.ts +65 -0
  170. package/src/scene/assets/fonts/TitanOne-OFL.txt +94 -0
  171. package/src/scene/assets/fonts/titan-one.woff2 +0 -0
  172. package/src/scene/assets/gsap/LICENSE.txt +8 -0
  173. package/src/scene/assets/gsap/gsap.min.js +11 -0
  174. package/src/scene/audio.ts +54 -0
  175. package/src/scene/blocks/LICENSE +190 -0
  176. package/src/scene/blocks/NOTICE +5 -0
  177. package/src/scene/blocks/bar-chart-race/bar-chart-race.html +585 -0
  178. package/src/scene/blocks/beat-freeze-cut/beat-freeze-cut.html +874 -0
  179. package/src/scene/blocks/camcorder-hud/camcorder-hud.html +164 -0
  180. package/src/scene/blocks/camera-dolly-zoom/camera-dolly-zoom.html +355 -0
  181. package/src/scene/blocks/code-diff/code-diff.html +817 -0
  182. package/src/scene/blocks/code-snippet-apple-terminal-pro/code-snippet-apple-terminal-pro.html +248 -0
  183. package/src/scene/blocks/code-typing/code-typing.html +764 -0
  184. package/src/scene/blocks/data-chart/data-chart.html +452 -0
  185. package/src/scene/blocks/flowchart-vertical/flowchart-vertical.html +454 -0
  186. package/src/scene/blocks/hw-scribble-transition/hw-scribble-transition.html +153 -0
  187. package/src/scene/blocks/index.json +824 -0
  188. package/src/scene/blocks/lt-accent-underline/lt-accent-underline.html +107 -0
  189. package/src/scene/blocks/lt-bold-block/lt-bold-block.html +109 -0
  190. package/src/scene/blocks/lt-clean-bar/lt-clean-bar.html +112 -0
  191. package/src/scene/blocks/lt-dark-card/lt-dark-card.html +111 -0
  192. package/src/scene/blocks/lt-kicker-name/lt-kicker-name.html +114 -0
  193. package/src/scene/blocks/lt-mask-reveal/lt-mask-reveal.html +120 -0
  194. package/src/scene/blocks/lt-neon-border/lt-neon-border.html +391 -0
  195. package/src/scene/blocks/lt-stack-bars/lt-stack-bars.html +109 -0
  196. package/src/scene/blocks/macos-notification/macos-notification.html +194 -0
  197. package/src/scene/blocks/mk-background/mk-background.html +236 -0
  198. package/src/scene/blocks/mk-callout-highlight/mk-callout-highlight.html +183 -0
  199. package/src/scene/blocks/mk-clone-wall-transition/mk-clone-wall-transition.html +226 -0
  200. package/src/scene/blocks/mk-line-graph/mk-line-graph.html +293 -0
  201. package/src/scene/blocks/mk-progress-stat/mk-progress-stat.html +186 -0
  202. package/src/scene/blocks/mk-specs-list/mk-specs-list.html +223 -0
  203. package/src/scene/blocks/news-ticker/news-ticker.html +409 -0
  204. package/src/scene/blocks/organic-light-leak-overlay/organic-light-leak-overlay.html +126 -0
  205. package/src/scene/blocks/oscilloscope-trace/oscilloscope-trace.html +519 -0
  206. package/src/scene/blocks/snow/snow.html +205 -0
  207. package/src/scene/blocks/split-flap-board/split-flap-board.html +347 -0
  208. package/src/scene/blocks/spotify-card/spotify-card.html +294 -0
  209. package/src/scene/blocks/starfield/starfield.html +188 -0
  210. package/src/scene/blocks/thread-message-stack/thread-message-stack.html +239 -0
  211. package/src/scene/blocks/week-in-merges/week-in-merges.html +470 -0
  212. package/src/scene/blocks/x-post/x-post.html +437 -0
  213. package/src/scene/blocks/yt-comment-card/yt-comment-card.html +319 -0
  214. package/src/scene/blocks/yt-lcd-background/yt-lcd-background.html +307 -0
  215. package/src/scene/blocks/yt-prism-title/yt-prism-title.html +205 -0
  216. package/src/scene/blocks.ts +229 -0
  217. package/src/scene/command.ts +272 -0
  218. package/src/scene/figure.ts +181 -0
  219. package/src/scene/glyphs.ts +107 -0
  220. package/src/scene/init.ts +86 -0
  221. package/src/scene/inspect/css-tree.d.ts +6 -0
  222. package/src/scene/inspect/page-scripts.ts +272 -0
  223. package/src/scene/inspect/report.ts +405 -0
  224. package/src/scene/inspect/run.ts +200 -0
  225. package/src/scene/inspect/source.ts +236 -0
  226. package/src/scene/label.ts +131 -0
  227. package/src/scene/net-guard.ts +74 -0
  228. package/src/scene/render.ts +292 -0
  229. package/src/scene/runtime/browser.ts +199 -0
  230. package/src/scene/runtime/core.ts +181 -0
  231. package/src/scene/runtime/react.tsx +115 -0
  232. package/src/scene/runtime/transitions.ts +133 -0
  233. package/src/scene/sanitize.ts +181 -0
  234. package/src/scene/scene.ts +152 -0
  235. package/src/scene/studio-command.ts +48 -0
  236. package/src/scene/studio-skin.ts +59 -0
  237. package/src/scene/studio.ts +337 -0
  238. package/src/scene/supply.ts +111 -0
  239. package/src/scene/timing.ts +288 -0
  240. package/src/scene/transitions/LICENSE +190 -0
  241. package/src/scene/transitions/NOTICE +5 -0
  242. package/src/scene/transitions/SOURCE.json +24 -0
  243. package/src/scene/transitions/transitions.global.js +38 -0
  244. package/src/scene/transitions.ts +26 -0
  245. package/src/service/all-projects.ts +153 -0
  246. package/src/service/commands.ts +256 -0
  247. package/src/service/due.ts +82 -0
  248. package/src/service/lock.ts +101 -0
  249. package/src/service/log-file.ts +56 -0
  250. package/src/service/state.ts +41 -0
  251. package/src/service/units.ts +90 -0
  252. package/src/service/worker.ts +51 -0
  253. package/src/skill-agents.ts +74 -0
  254. package/src/skill-install.ts +334 -0
  255. package/src/sync-write.ts +21 -0
  256. package/src/tick-report.ts +205 -0
  257. package/src/tts.ts +124 -0
  258. package/vendor/@vfrey/contracts/dist/account.d.ts +97 -0
  259. package/vendor/@vfrey/contracts/dist/account.js +27 -0
  260. package/vendor/@vfrey/contracts/dist/checks.d.ts +8 -0
  261. package/vendor/@vfrey/contracts/dist/checks.js +1 -0
  262. package/vendor/@vfrey/contracts/dist/credentials/crypto.d.ts +4 -0
  263. package/vendor/@vfrey/contracts/dist/credentials/crypto.js +38 -0
  264. package/vendor/@vfrey/contracts/dist/domain/advice-persona.d.ts +10 -0
  265. package/vendor/@vfrey/contracts/dist/domain/advice-persona.js +24 -0
  266. package/vendor/@vfrey/contracts/dist/domain/backoff.d.ts +13 -0
  267. package/vendor/@vfrey/contracts/dist/domain/backoff.js +27 -0
  268. package/vendor/@vfrey/contracts/dist/domain/cadence.d.ts +29 -0
  269. package/vendor/@vfrey/contracts/dist/domain/cadence.js +95 -0
  270. package/vendor/@vfrey/contracts/dist/domain/creators.d.ts +146 -0
  271. package/vendor/@vfrey/contracts/dist/domain/creators.js +2 -0
  272. package/vendor/@vfrey/contracts/dist/domain/disclosure.d.ts +127 -0
  273. package/vendor/@vfrey/contracts/dist/domain/disclosure.js +250 -0
  274. package/vendor/@vfrey/contracts/dist/domain/errors.d.ts +34 -0
  275. package/vendor/@vfrey/contracts/dist/domain/errors.js +68 -0
  276. package/vendor/@vfrey/contracts/dist/domain/redact.d.ts +1 -0
  277. package/vendor/@vfrey/contracts/dist/domain/redact.js +21 -0
  278. package/vendor/@vfrey/contracts/dist/domain/revenue.d.ts +13 -0
  279. package/vendor/@vfrey/contracts/dist/domain/revenue.js +16 -0
  280. package/vendor/@vfrey/contracts/dist/domain/safety.d.ts +20 -0
  281. package/vendor/@vfrey/contracts/dist/domain/safety.js +61 -0
  282. package/vendor/@vfrey/contracts/dist/domain/similarity.d.ts +13 -0
  283. package/vendor/@vfrey/contracts/dist/domain/similarity.js +100 -0
  284. package/vendor/@vfrey/contracts/dist/domain/templates.d.ts +18 -0
  285. package/vendor/@vfrey/contracts/dist/domain/templates.js +67 -0
  286. package/vendor/@vfrey/contracts/dist/domain/tiktok-chunks.d.ts +19 -0
  287. package/vendor/@vfrey/contracts/dist/domain/tiktok-chunks.js +52 -0
  288. package/vendor/@vfrey/contracts/dist/domain/topics.d.ts +78 -0
  289. package/vendor/@vfrey/contracts/dist/domain/topics.js +4 -0
  290. package/vendor/@vfrey/contracts/dist/domain/types.d.ts +216 -0
  291. package/vendor/@vfrey/contracts/dist/domain/types.js +1 -0
  292. package/vendor/@vfrey/contracts/dist/domain/variation.d.ts +57 -0
  293. package/vendor/@vfrey/contracts/dist/domain/variation.js +278 -0
  294. package/vendor/@vfrey/contracts/dist/events.d.ts +20 -0
  295. package/vendor/@vfrey/contracts/dist/events.js +1 -0
  296. package/vendor/@vfrey/contracts/dist/http.d.ts +11 -0
  297. package/vendor/@vfrey/contracts/dist/http.js +37 -0
  298. package/vendor/@vfrey/contracts/dist/index.d.ts +22 -0
  299. package/vendor/@vfrey/contracts/dist/index.js +22 -0
  300. package/vendor/@vfrey/contracts/dist/log.d.ts +2 -0
  301. package/vendor/@vfrey/contracts/dist/log.js +10 -0
  302. package/vendor/@vfrey/contracts/dist/mail.d.ts +55 -0
  303. package/vendor/@vfrey/contracts/dist/mail.js +1 -0
  304. package/vendor/@vfrey/contracts/package.json +18 -0
  305. package/vendor/@vfrey/ingest/dist/index.d.ts +41 -0
  306. package/vendor/@vfrey/ingest/dist/index.js +221 -0
  307. package/vendor/@vfrey/ingest/dist/parse.d.ts +5 -0
  308. package/vendor/@vfrey/ingest/dist/parse.js +165 -0
  309. package/vendor/@vfrey/ingest/package.json +21 -0
  310. package/vendor/framey/LICENSE +190 -0
  311. package/vendor/framey/NOTICE +15 -0
  312. package/vendor/framey/SOURCE.json +24 -0
  313. package/vendor/framey/STUDIO-SOURCE.json +9 -0
  314. package/vendor/framey/bin/framey-localize-fonts.mjs +12 -0
  315. package/vendor/framey/bin/framey.mjs +16 -0
  316. package/vendor/framey/dist/add-VMJD5KBS.js +75 -0
  317. package/vendor/framey/dist/animatedGifPrep-QPKGJ6PM.js +43 -0
  318. package/vendor/framey/dist/api.d.ts +211 -0
  319. package/vendor/framey/dist/api.js +76 -0
  320. package/vendor/framey/dist/appHistory-WW56FZTT.js +31 -0
  321. package/vendor/framey/dist/asset-WQKGOELE.js +26 -0
  322. package/vendor/framey/dist/audio-RSJWVWRA.js +1936 -0
  323. package/vendor/framey/dist/batchRender-3HPICDCT.js +366 -0
  324. package/vendor/framey/dist/beats-HL2BDUZI.js +273 -0
  325. package/vendor/framey/dist/benchmark-RWCWHI5Z.js +230 -0
  326. package/vendor/framey/dist/browser-ECYET3Q2.js +206 -0
  327. package/vendor/framey/dist/bundleWithLocalizedFonts-ALYBQEQT.js +52 -0
  328. package/vendor/framey/dist/captureCompositionFrame-4DA6SEH2.js +94 -0
  329. package/vendor/framey/dist/catalog-5JVDEUWK.js +979 -0
  330. package/vendor/framey/dist/catch-up-NEBCL5NO.js +106 -0
  331. package/vendor/framey/dist/check-F5N544ML.js +111 -0
  332. package/vendor/framey/dist/checkBrowser-LZ66CMJG.js +110 -0
  333. package/vendor/framey/dist/chunk-2K7Z2HVP.js +372 -0
  334. package/vendor/framey/dist/chunk-2UZQHDM7.js +323 -0
  335. package/vendor/framey/dist/chunk-2X4PQEYM.js +34 -0
  336. package/vendor/framey/dist/chunk-3422KMTD.js +168 -0
  337. package/vendor/framey/dist/chunk-3FYD4YEK.js +72 -0
  338. package/vendor/framey/dist/chunk-3GXQPCJC.js +682 -0
  339. package/vendor/framey/dist/chunk-3LM6YPBR.js +179 -0
  340. package/vendor/framey/dist/chunk-442JNXRM.js +20 -0
  341. package/vendor/framey/dist/chunk-4BNN5FGX.js +182 -0
  342. package/vendor/framey/dist/chunk-4P7NX5BM.js +11532 -0
  343. package/vendor/framey/dist/chunk-4UMPIPCQ.js +88 -0
  344. package/vendor/framey/dist/chunk-4YYJOFKS.js +28 -0
  345. package/vendor/framey/dist/chunk-54HJ63Q2.js +29 -0
  346. package/vendor/framey/dist/chunk-5ETJX6WM.js +89 -0
  347. package/vendor/framey/dist/chunk-5SG54JRS.js +68 -0
  348. package/vendor/framey/dist/chunk-634HSJ3J.js +58 -0
  349. package/vendor/framey/dist/chunk-66LREXGS.js +48 -0
  350. package/vendor/framey/dist/chunk-6BEFEWFB.js +25 -0
  351. package/vendor/framey/dist/chunk-6BNTGNGD.js +286 -0
  352. package/vendor/framey/dist/chunk-6DDPOHMK.js +140 -0
  353. package/vendor/framey/dist/chunk-6GV5XSOR.js +214 -0
  354. package/vendor/framey/dist/chunk-6JR2XUEE.js +127 -0
  355. package/vendor/framey/dist/chunk-73QVATIM.js +362 -0
  356. package/vendor/framey/dist/chunk-7FMUTGOH.js +279 -0
  357. package/vendor/framey/dist/chunk-7JQFJ6JJ.js +291 -0
  358. package/vendor/framey/dist/chunk-AHPGAESX.js +113 -0
  359. package/vendor/framey/dist/chunk-AHX34EFP.js +15 -0
  360. package/vendor/framey/dist/chunk-AJ5CSAEZ.js +487 -0
  361. package/vendor/framey/dist/chunk-B7RJV74O.js +5047 -0
  362. package/vendor/framey/dist/chunk-BL4U6U52.js +254 -0
  363. package/vendor/framey/dist/chunk-BSQ7XANM.js +210 -0
  364. package/vendor/framey/dist/chunk-BZ56MTDX.js +6 -0
  365. package/vendor/framey/dist/chunk-C5BO7PMT.js +282 -0
  366. package/vendor/framey/dist/chunk-CFUCHO4T.js +358 -0
  367. package/vendor/framey/dist/chunk-CZGHFATT.js +31 -0
  368. package/vendor/framey/dist/chunk-DLA44CMS.js +5649 -0
  369. package/vendor/framey/dist/chunk-DPVXBVLL.js +31292 -0
  370. package/vendor/framey/dist/chunk-DQBY7FRR.js +397 -0
  371. package/vendor/framey/dist/chunk-EAMLBAHA.js +154 -0
  372. package/vendor/framey/dist/chunk-EOVYQ3IG.js +143 -0
  373. package/vendor/framey/dist/chunk-EVMY7IPL.js +612 -0
  374. package/vendor/framey/dist/chunk-FG4BBFZI.js +122 -0
  375. package/vendor/framey/dist/chunk-FI4I7CME.js +56 -0
  376. package/vendor/framey/dist/chunk-FLREUEHW.js +2081 -0
  377. package/vendor/framey/dist/chunk-FUFP7A7S.js +1126 -0
  378. package/vendor/framey/dist/chunk-FV3PIKGH.js +56 -0
  379. package/vendor/framey/dist/chunk-FXMKHHPO.js +123 -0
  380. package/vendor/framey/dist/chunk-GGZAM5VH.js +1187 -0
  381. package/vendor/framey/dist/chunk-GKZEVJIB.js +39 -0
  382. package/vendor/framey/dist/chunk-GMECXFAN.js +57 -0
  383. package/vendor/framey/dist/chunk-GNEFDLFB.js +621 -0
  384. package/vendor/framey/dist/chunk-GPMLSOEG.js +19274 -0
  385. package/vendor/framey/dist/chunk-GPYVT5EE.js +59 -0
  386. package/vendor/framey/dist/chunk-H6IEMZEV.js +40 -0
  387. package/vendor/framey/dist/chunk-HEFQCIXR.js +170 -0
  388. package/vendor/framey/dist/chunk-HFQ4CWHA.js +118 -0
  389. package/vendor/framey/dist/chunk-HPDTCOKE.js +304 -0
  390. package/vendor/framey/dist/chunk-HTUHQKDB.js +548 -0
  391. package/vendor/framey/dist/chunk-HU2SPFNG.js +103 -0
  392. package/vendor/framey/dist/chunk-HVW7ABXD.js +19 -0
  393. package/vendor/framey/dist/chunk-IG7NDKXO.js +26 -0
  394. package/vendor/framey/dist/chunk-II3J3WXK.js +246 -0
  395. package/vendor/framey/dist/chunk-IR35BCLK.js +13 -0
  396. package/vendor/framey/dist/chunk-ISN5GRYI.js +20 -0
  397. package/vendor/framey/dist/chunk-IUONB55J.js +669 -0
  398. package/vendor/framey/dist/chunk-IVSUQR2B.js +161 -0
  399. package/vendor/framey/dist/chunk-IXU6PQSW.js +259 -0
  400. package/vendor/framey/dist/chunk-IZCW2NKT.js +29 -0
  401. package/vendor/framey/dist/chunk-JGXJU5PF.js +68 -0
  402. package/vendor/framey/dist/chunk-JHOCK3DG.js +27 -0
  403. package/vendor/framey/dist/chunk-JVBRJMOP.js +121 -0
  404. package/vendor/framey/dist/chunk-K4MJX7SR.js +17 -0
  405. package/vendor/framey/dist/chunk-K4WKGXRC.js +18 -0
  406. package/vendor/framey/dist/chunk-KGPLIH7B.js +127 -0
  407. package/vendor/framey/dist/chunk-KLIKCX6Q.js +63 -0
  408. package/vendor/framey/dist/chunk-KXTKKBOC.js +61 -0
  409. package/vendor/framey/dist/chunk-KZI44OPA.js +185 -0
  410. package/vendor/framey/dist/chunk-LEKZNRKI.js +45 -0
  411. package/vendor/framey/dist/chunk-LFGJWOVA.js +61 -0
  412. package/vendor/framey/dist/chunk-LLMSX6V6.js +17 -0
  413. package/vendor/framey/dist/chunk-LTGTN2TH.js +195 -0
  414. package/vendor/framey/dist/chunk-M2WBOQ2Q.js +54 -0
  415. package/vendor/framey/dist/chunk-MI5FTLYK.js +399 -0
  416. package/vendor/framey/dist/chunk-MLQP2QUF.js +1809 -0
  417. package/vendor/framey/dist/chunk-MMQ52NIZ.js +956 -0
  418. package/vendor/framey/dist/chunk-ND46XKK3.js +742 -0
  419. package/vendor/framey/dist/chunk-NYEXNSHM.js +504 -0
  420. package/vendor/framey/dist/chunk-O4YRB27L.js +57 -0
  421. package/vendor/framey/dist/chunk-O7BGSQ5F.js +92 -0
  422. package/vendor/framey/dist/chunk-OFQWTKPW.js +117 -0
  423. package/vendor/framey/dist/chunk-P5ZBLXJ6.js +91 -0
  424. package/vendor/framey/dist/chunk-P6LJ4Y32.js +4662 -0
  425. package/vendor/framey/dist/chunk-PKZ7RK5N.js +281 -0
  426. package/vendor/framey/dist/chunk-PURHAXFL.js +15 -0
  427. package/vendor/framey/dist/chunk-Q5X2B5WT.js +29 -0
  428. package/vendor/framey/dist/chunk-QAMKOG67.js +248 -0
  429. package/vendor/framey/dist/chunk-R6YBK4XX.js +514 -0
  430. package/vendor/framey/dist/chunk-RGLQURIX.js +2997 -0
  431. package/vendor/framey/dist/chunk-RNV3DM4S.js +3573 -0
  432. package/vendor/framey/dist/chunk-ROEW726S.js +4638 -0
  433. package/vendor/framey/dist/chunk-RQVDTPGD.js +6 -0
  434. package/vendor/framey/dist/chunk-RVZHGOGR.js +26 -0
  435. package/vendor/framey/dist/chunk-RXATE6FZ.js +385 -0
  436. package/vendor/framey/dist/chunk-S3S4TGEH.js +424 -0
  437. package/vendor/framey/dist/chunk-SDYDNHEE.js +11202 -0
  438. package/vendor/framey/dist/chunk-SSWBU67I.js +173 -0
  439. package/vendor/framey/dist/chunk-ST35C4K6.js +28 -0
  440. package/vendor/framey/dist/chunk-SZNTYTIV.js +990 -0
  441. package/vendor/framey/dist/chunk-T2EUWCJE.js +29 -0
  442. package/vendor/framey/dist/chunk-T6AGUTZJ.js +444 -0
  443. package/vendor/framey/dist/chunk-THJALHFJ.js +11131 -0
  444. package/vendor/framey/dist/chunk-TIQB63MX.js +18128 -0
  445. package/vendor/framey/dist/chunk-U242RTCT.js +134 -0
  446. package/vendor/framey/dist/chunk-UJZREAV6.js +3901 -0
  447. package/vendor/framey/dist/chunk-UMD3V7CU.js +264 -0
  448. package/vendor/framey/dist/chunk-UOFAE5RF.js +42 -0
  449. package/vendor/framey/dist/chunk-UTEIUGAZ.js +29 -0
  450. package/vendor/framey/dist/chunk-VAS6HQX3.js +138 -0
  451. package/vendor/framey/dist/chunk-VC76R6W2.js +27 -0
  452. package/vendor/framey/dist/chunk-VFZ7W5W7.js +226 -0
  453. package/vendor/framey/dist/chunk-VHNLZAE2.js +348 -0
  454. package/vendor/framey/dist/chunk-VMC4WSH5.js +278 -0
  455. package/vendor/framey/dist/chunk-VORQIH64.js +108 -0
  456. package/vendor/framey/dist/chunk-VTXFRIHO.js +186 -0
  457. package/vendor/framey/dist/chunk-W2LGJSEN.js +182 -0
  458. package/vendor/framey/dist/chunk-WKBM7NDG.js +32 -0
  459. package/vendor/framey/dist/chunk-WMQNIQDJ.js +2057 -0
  460. package/vendor/framey/dist/chunk-WN4XZC5F.js +504 -0
  461. package/vendor/framey/dist/chunk-WQ6ZXHFQ.js +133 -0
  462. package/vendor/framey/dist/chunk-X7HBSTCZ.js +53 -0
  463. package/vendor/framey/dist/chunk-X7MTZ4T2.js +6 -0
  464. package/vendor/framey/dist/chunk-XPTOJZJF.js +308 -0
  465. package/vendor/framey/dist/chunk-XT6MXC7A.js +6 -0
  466. package/vendor/framey/dist/chunk-XT7X6QCP.js +6 -0
  467. package/vendor/framey/dist/chunk-XTMDWNEC.js +2820 -0
  468. package/vendor/framey/dist/chunk-XUHOIDKG.js +6 -0
  469. package/vendor/framey/dist/chunk-XX7LTW5Y.js +138 -0
  470. package/vendor/framey/dist/chunk-Y3YGTZEG.js +382 -0
  471. package/vendor/framey/dist/chunk-Y5SBSGN7.js +346 -0
  472. package/vendor/framey/dist/chunk-YCMVI4MZ.js +625 -0
  473. package/vendor/framey/dist/chunk-YOR4CTCE.js +261 -0
  474. package/vendor/framey/dist/chunk-YPRBGITP.js +2748 -0
  475. package/vendor/framey/dist/chunk-ZKDMAOFU.js +32 -0
  476. package/vendor/framey/dist/chunk-ZW6DURPF.js +1318 -0
  477. package/vendor/framey/dist/clean-ZJOGDKDM.js +336 -0
  478. package/vendor/framey/dist/cli.js +439 -0
  479. package/vendor/framey/dist/cloudrun-HGJ3B6ZV.js +767 -0
  480. package/vendor/framey/dist/commands/contrast-audit.browser.js +643 -0
  481. package/vendor/framey/dist/commands/layout-audit.browser.js +2914 -0
  482. package/vendor/framey/dist/commands/motion-sample.browser.js +74 -0
  483. package/vendor/framey/dist/commands/motion-signature.browser.js +475 -0
  484. package/vendor/framey/dist/compare-W7UY3BAZ.js +463 -0
  485. package/vendor/framey/dist/compiler-TM3W6GYF.js +136 -0
  486. package/vendor/framey/dist/component-QF5VGYMU.js +176 -0
  487. package/vendor/framey/dist/compositions-XT3PMNDF.js +238 -0
  488. package/vendor/framey/dist/contactSheet-7W6GYHBQ.js +24 -0
  489. package/vendor/framey/dist/deploy-6FJPWKF2.js +299 -0
  490. package/vendor/framey/dist/destroy-HE2RIPKZ.js +57 -0
  491. package/vendor/framey/dist/deterministicFonts-RADD3BSX.js +50 -0
  492. package/vendor/framey/dist/dist-4S2FGGVY.js +422 -0
  493. package/vendor/framey/dist/dist-BE4B2EZE.js +32 -0
  494. package/vendor/framey/dist/dist-EWXJGN63.js +129 -0
  495. package/vendor/framey/dist/docs-VKIRFEEX.js +131 -0
  496. package/vendor/framey/dist/doctor-2UNJ2G7U.js +74 -0
  497. package/vendor/framey/dist/dom-37K6YOH4.js +14 -0
  498. package/vendor/framey/dist/dotEnv-7PZQTVIM.js +54 -0
  499. package/vendor/framey/dist/events-ACY3P2PA.js +76 -0
  500. package/vendor/framey/dist/events-IHB3B2MK.js +69 -0
  501. package/vendor/framey/dist/ffmpeg-DR5LGGSK.js +63 -0
  502. package/vendor/framey/dist/figma-BODVUPHB.js +63 -0
  503. package/vendor/framey/dist/fontData.generated-NXAOI4GH.js +13 -0
  504. package/vendor/framey/dist/fontLocalizeCli.js +142 -0
  505. package/vendor/framey/dist/gpuPolicy-XLEXORZY.js +49 -0
  506. package/vendor/framey/dist/grade-compare-GAXFQ74C.js +661 -0
  507. package/vendor/framey/dist/gsapParser-IEUSLYDJ.js +2885 -0
  508. package/vendor/framey/dist/gsapParserAcorn-LZJKCFQI.js +30 -0
  509. package/vendor/framey/dist/help-VCB5X2HS.js +184 -0
  510. package/vendor/framey/dist/history-ENGZEVFT.js +471 -0
  511. package/vendor/framey/dist/hyperframe.manifest.json +22 -0
  512. package/vendor/framey/dist/hyperframe.runtime.iife.js +1132 -0
  513. package/vendor/framey/dist/hyperframes-player.global.js +478 -0
  514. package/vendor/framey/dist/hyperframes-slideshow.global.js +148 -0
  515. package/vendor/framey/dist/info-WPCJHBMU.js +182 -0
  516. package/vendor/framey/dist/init-G4WGLYGD.js +1176 -0
  517. package/vendor/framey/dist/inspect-5IQ6CNEQ.js +106 -0
  518. package/vendor/framey/dist/installer-ML4HWUV7.js +40 -0
  519. package/vendor/framey/dist/keyframes-Q6XJO7VW.js +2755 -0
  520. package/vendor/framey/dist/lambda-A7JC74JF.js +468 -0
  521. package/vendor/framey/dist/layout-YET6XNXA.js +95 -0
  522. package/vendor/framey/dist/lint-I7FZN2VJ.js +162 -0
  523. package/vendor/framey/dist/lintProject-PDB3UN72.js +30 -0
  524. package/vendor/framey/dist/manager-CD6LJ7ZN.js +45 -0
  525. package/vendor/framey/dist/manager-WA4F2GST.js +67 -0
  526. package/vendor/framey/dist/media-treatment-BXYS4WVA.js +1030 -0
  527. package/vendor/framey/dist/media-use-LWH3MANK.js +113 -0
  528. package/vendor/framey/dist/models-K363FYM4.js +195 -0
  529. package/vendor/framey/dist/motionShot-GPAVWXLH.js +640 -0
  530. package/vendor/framey/dist/normalize-FASIPBUM.js +24 -0
  531. package/vendor/framey/dist/normalize-audio-VDWYRHN6.js +268 -0
  532. package/vendor/framey/dist/open-E3TB6P6J.js +128 -0
  533. package/vendor/framey/dist/parakeetRunner-UP34UOI2.js +58 -0
  534. package/vendor/framey/dist/pipeline-VXIUYEBU.js +64 -0
  535. package/vendor/framey/dist/play-BUTDQ4DV.js +353 -0
  536. package/vendor/framey/dist/policies-OLDYGFK5.js +309 -0
  537. package/vendor/framey/dist/preflight-2KZ2GWAZ.js +55 -0
  538. package/vendor/framey/dist/present-GXYSM7NX.js +353 -0
  539. package/vendor/framey/dist/preview-EKZ33JOJ.js +1910 -0
  540. package/vendor/framey/dist/producer-SKU5EV3J.js +13 -0
  541. package/vendor/framey/dist/progress-ZWB4Z4ET.js +76 -0
  542. package/vendor/framey/dist/projectConfig-3MZAYVTS.js +60 -0
  543. package/vendor/framey/dist/publication-DTKWNF74.js +33 -0
  544. package/vendor/framey/dist/remove-background-OY5L7XN2.js +265 -0
  545. package/vendor/framey/dist/render-CDV4GDFX.js +187 -0
  546. package/vendor/framey/dist/render-YE5YAMOD.js +2620 -0
  547. package/vendor/framey/dist/render-batch-ZAMFPCLL.js +274 -0
  548. package/vendor/framey/dist/renderSetupWorker.js +120 -0
  549. package/vendor/framey/dist/resolver-D5BIPSEW.js +42 -0
  550. package/vendor/framey/dist/runtimeVersion.js +18 -0
  551. package/vendor/framey/dist/sam-WFZXI4F7.js +23 -0
  552. package/vendor/framey/dist/shaderTransitionWorker.js +44 -0
  553. package/vendor/framey/dist/sherpa-RQWLV7I3.js +79 -0
  554. package/vendor/framey/dist/sherpaWorker.js +84 -0
  555. package/vendor/framey/dist/sites-AMOVABTK.js +54 -0
  556. package/vendor/framey/dist/skill-ZHVLH7KX.js +15 -0
  557. package/vendor/framey/dist/slideshow-SJGK4RQS.js +24 -0
  558. package/vendor/framey/dist/snapshot-TUXL6LBQ.js +1515 -0
  559. package/vendor/framey/dist/src-JHQGB2S6.js +215 -0
  560. package/vendor/framey/dist/src-Z5PGEMXB.js +532 -0
  561. package/vendor/framey/dist/state-B6H3WHCF.js +28 -0
  562. package/vendor/framey/dist/staticProjectServer-DFVZOUGI.js +63 -0
  563. package/vendor/framey/dist/studio/assets/hyperframes-player-DLaKjw0L.js +478 -0
  564. package/vendor/framey/dist/studio/assets/index-BX3KHhGX.js +71 -0
  565. package/vendor/framey/dist/studio/assets/index-CYbvOVp-.css +1 -0
  566. package/vendor/framey/dist/studio/assets/index-Dp3QEUGL.js +402 -0
  567. package/vendor/framey/dist/studio/assets/index-qrTAXbqc.js +531 -0
  568. package/vendor/framey/dist/studio/favicon.svg +14 -0
  569. package/vendor/framey/dist/studio/icons/timeline/audio.svg +7 -0
  570. package/vendor/framey/dist/studio/icons/timeline/captions.svg +5 -0
  571. package/vendor/framey/dist/studio/icons/timeline/composition.svg +12 -0
  572. package/vendor/framey/dist/studio/icons/timeline/image.svg +18 -0
  573. package/vendor/framey/dist/studio/icons/timeline/music.svg +10 -0
  574. package/vendor/framey/dist/studio/icons/timeline/text.svg +3 -0
  575. package/vendor/framey/dist/studio/index.html +23 -0
  576. package/vendor/framey/dist/studio-api-2NECGRMB.js +179 -0
  577. package/vendor/framey/dist/studioSelectionClient-IUUPKUHZ.js +25 -0
  578. package/vendor/framey/dist/studioServer-N2OPUMOQ.js +1569 -0
  579. package/vendor/framey/dist/synthesize-PKTB5ATG.js +183 -0
  580. package/vendor/framey/dist/system-7F3UEHQT.js +27 -0
  581. package/vendor/framey/dist/telemetry-DQGPEKM6.js +145 -0
  582. package/vendor/framey/dist/telemetry-UN55N6U6.js +77 -0
  583. package/vendor/framey/dist/timeline-LHUUMK63.js +1295 -0
  584. package/vendor/framey/dist/tokens-OLECWDFK.js +92 -0
  585. package/vendor/framey/dist/transcribe-O7HQKMIV.js +331 -0
  586. package/vendor/framey/dist/transcribe-UJESPBT5.js +76 -0
  587. package/vendor/framey/dist/tts-YCCQHZD4.js +234 -0
  588. package/vendor/framey/dist/urlDownloader-H2LXLG4U.js +48 -0
  589. package/vendor/framey/dist/usage-HG4WMM7A.js +2167 -0
  590. package/vendor/framey/dist/validate-F2XMN6EV.js +87 -0
  591. package/vendor/framey/dist/vendor/npm/gsap@3.15.0/GSAP-LICENSE.txt +8 -0
  592. package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/CustomEase.min.js +11 -0
  593. package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/MotionPathPlugin.min.js +11 -0
  594. package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/gsap.min.js +11 -0
  595. package/vendor/framey/dist/warmCheck.js +220 -0
  596. package/vendor/framey/dist/warmCheckClient.js +139 -0
  597. package/vendor/framey/dist/workspaceBuildCheck-XYD2YTFW.js +133 -0
  598. package/vendor/framey-player/LICENSE +190 -0
  599. package/vendor/framey-player/NOTICE +16 -0
  600. package/vendor/framey-player/SOURCE.json +6 -0
  601. package/vendor/framey-player/hyperframes-player.global.js +478 -0
  602. package/vendor/framey.pin +1 -0
  603. package/bin/index.js +0 -3
@@ -0,0 +1,2748 @@
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
+ pinProjectRecord,
9
+ realFilePath
10
+ } from "./chunk-7JQFJ6JJ.js";
11
+ import {
12
+ atomicTempPath,
13
+ isAtomicTempPath,
14
+ replaceFileAtomically
15
+ } from "./chunk-EOVYQ3IG.js";
16
+ import {
17
+ isProjectIgnoredDir
18
+ } from "./chunk-EVMY7IPL.js";
19
+ import {
20
+ isSafePath,
21
+ mkdirWithinProject
22
+ } from "./chunk-OFQWTKPW.js";
23
+
24
+ // ../studio-server/dist/chunk-TNYZG57Q.js
25
+ import { randomUUID as randomUUID7 } from "crypto";
26
+ import { mkdir as mkdir2, readdir as readdir3, rm as rm3, rmdir, stat as stat2, writeFile } from "fs/promises";
27
+ import {
28
+ existsSync as existsSync3,
29
+ lstatSync as lstatSync4,
30
+ statSync as statSync5,
31
+ mkdirSync as mkdirSync5,
32
+ readdirSync as readdirSync4,
33
+ readFileSync as readFileSync10,
34
+ realpathSync,
35
+ rmSync as rmSync3
36
+ } from "fs";
37
+ import { homedir } from "os";
38
+ import { dirname as dirname5, isAbsolute as isAbsolute5, join as join8, relative as relative5, resolve as resolve7, sep as sep5 } from "path";
39
+ import { createHash, randomUUID } from "crypto";
40
+ import { readFileSync as readFileSync2 } from "fs";
41
+ import { isAbsolute, relative, resolve, sep } from "path";
42
+ import { randomUUID as randomUUID2 } from "crypto";
43
+ import {
44
+ linkSync,
45
+ mkdirSync,
46
+ readFileSync,
47
+ renameSync,
48
+ rmSync,
49
+ statSync,
50
+ writeFileSync
51
+ } from "fs";
52
+ import { join } from "path";
53
+ import { createHash as createHash2 } from "crypto";
54
+ import { lstatSync, readFileSync as readFileSync3, readdirSync } from "fs";
55
+ import { extname, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
56
+ import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
57
+ import { constants, createReadStream, createWriteStream, renameSync as renameSync2 } from "fs";
58
+ import { copyFile, mkdir, readdir, readFile, rename, rm, stat } from "fs/promises";
59
+ import { dirname, join as join2 } from "path";
60
+ import { pipeline } from "stream/promises";
61
+ import { extname as extname2 } from "path";
62
+ import { randomUUID as randomUUID5 } from "crypto";
63
+ import { readdirSync as readdirSync2, readFileSync as readFileSync5, renameSync as renameSync3, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
64
+ import { lstat, readdir as readdir2, rm as rm2 } from "fs/promises";
65
+ import { tmpdir } from "os";
66
+ import { dirname as dirname2, join as join4 } from "path";
67
+ import { randomUUID as randomUUID4 } from "crypto";
68
+ import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync4, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
69
+ import { basename, join as join3, resolve as resolve3 } from "path";
70
+ import { createHash as createHash5, randomUUID as randomUUID6 } from "crypto";
71
+ import { createReadStream as createReadStream2, existsSync as existsSync2, readFileSync as readFileSync8, renameSync as renameSync4, statSync as statSync4 } from "fs";
72
+ import { isAbsolute as isAbsolute4, join as join7, resolve as resolve6 } from "path";
73
+ import { createHash as createHash4 } from "crypto";
74
+ import {
75
+ closeSync,
76
+ constants as constants2,
77
+ fstatSync,
78
+ lstatSync as lstatSync2,
79
+ openSync,
80
+ readFileSync as readFileSync6,
81
+ unlinkSync
82
+ } from "fs";
83
+ import { isAbsolute as isAbsolute3, join as join5, relative as relative3, resolve as resolve4, sep as sep3 } from "path";
84
+ import {
85
+ closeSync as closeSync2,
86
+ constants as constants3,
87
+ fstatSync as fstatSync2,
88
+ lstatSync as lstatSync3,
89
+ openSync as openSync2,
90
+ mkdirSync as mkdirSync3,
91
+ readFileSync as readFileSync7,
92
+ readdirSync as readdirSync3,
93
+ rmSync as rmSync2,
94
+ ftruncateSync,
95
+ writeSync,
96
+ writeFileSync as writeFileSync4
97
+ } from "fs";
98
+ import { dirname as dirname3, join as join6, relative as relative4, resolve as resolve5, sep as sep4 } from "path";
99
+ import { closeSync as closeSync3, constants as constants4, mkdirSync as mkdirSync4, openSync as openSync3, readFileSync as readFileSync9, writeSync as writeSync2 } from "fs";
100
+ import { dirname as dirname4 } from "path";
101
+ import { readdirSync as readdirSync5 } from "fs";
102
+ import { rm as rm4, stat as stat3 } from "fs/promises";
103
+ import { join as join9 } from "path";
104
+ var RECEIPT_TTL_MS = 1e4;
105
+ var receipts = /* @__PURE__ */ new Map();
106
+ var overwriteListeners = /* @__PURE__ */ new Set();
107
+ function fileContentVersion(content) {
108
+ return hashVersion(createHash("sha256").update(content).digest("hex"));
109
+ }
110
+ var DELETED_VERSION = '"deleted"';
111
+ function hashVersion(hex) {
112
+ return `"sha256:${hex}"`;
113
+ }
114
+ function hashOfVersion(version) {
115
+ return /^"sha256:([0-9a-f]{64})"$/.exec(version)?.[1];
116
+ }
117
+ function settledFileTag(stat4, now = Date.now()) {
118
+ if (now - stat4.ctimeMs < 3e3) return null;
119
+ return [stat4.ino, stat4.ctimeMs, stat4.size].map((n) => n.toString(36)).join("-");
120
+ }
121
+ function createWriteToken(requestToken) {
122
+ const token = requestToken?.trim();
123
+ return token && token.length <= 200 ? token : randomUUID();
124
+ }
125
+ function onFileOverwritten(listener) {
126
+ overwriteListeners.add(listener);
127
+ return () => overwriteListeners.delete(listener);
128
+ }
129
+ function recordFileWriteReceipt(filePath, {
130
+ overwrote,
131
+ path,
132
+ version,
133
+ writeToken
134
+ }) {
135
+ const receipt = { path, version, writeToken };
136
+ const absPath = realFilePath(filePath);
137
+ if (overwrote !== void 0)
138
+ for (const listener of overwriteListeners) listener(absPath, receipt.version, overwrote);
139
+ const now = Date.now();
140
+ for (const [path2, list] of receipts) {
141
+ const live = list.filter((entry) => now - entry.recordedAt < RECEIPT_TTL_MS);
142
+ if (live.length > 0) receipts.set(path2, live);
143
+ else receipts.delete(path2);
144
+ }
145
+ const from = overwrote === void 0 ? void 0 : fileContentVersion(overwrote);
146
+ receipts.set(absPath, [
147
+ ...receipts.get(absPath) ?? [],
148
+ { ...receipt, ...from && { from }, recordedAt: now }
149
+ ]);
150
+ }
151
+ function clearFileWriteReceipt(filePath, version, writeToken) {
152
+ const absPath = realFilePath(filePath);
153
+ const current = (receipts.get(absPath) ?? []).filter(
154
+ (entry) => entry.version !== version || entry.writeToken !== writeToken
155
+ );
156
+ if (current.length > 0) receipts.set(absPath, current);
157
+ else receipts.delete(absPath);
158
+ }
159
+ function identifyFileWrite(filePath, expectedVersion) {
160
+ const receipt = newestReceipt(realFilePath(filePath), expectedVersion);
161
+ if (!receipt) return null;
162
+ const { path, version, writeToken, from } = receipt;
163
+ return { path, version, writeToken, ...from && { from } };
164
+ }
165
+ function newestReceipt(absPath, expectedVersion) {
166
+ const now = Date.now();
167
+ const current = (receipts.get(absPath) ?? []).filter(
168
+ (entry) => now - entry.recordedAt < RECEIPT_TTL_MS
169
+ );
170
+ if (current.length > 0) receipts.set(absPath, current);
171
+ else receipts.delete(absPath);
172
+ let receipt;
173
+ for (let i = current.length - 1; i >= 0 && !receipt; i -= 1) {
174
+ if (current[i]?.version === expectedVersion) receipt = current[i];
175
+ }
176
+ return receipt;
177
+ }
178
+ var consumeFileWriteReceipt = identifyFileWrite;
179
+ var HistoryBusyError = class extends Error {
180
+ constructor(pid, url) {
181
+ super(`This project's history is open in another process (pid ${pid}).`);
182
+ this.pid = pid;
183
+ this.url = url;
184
+ this.name = "HistoryBusyError";
185
+ }
186
+ pid;
187
+ url;
188
+ };
189
+ var WINDOWS_BUSY = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES"]);
190
+ var NO_HARD_LINKS = /* @__PURE__ */ new Set(["ENOTSUP", "EOPNOTSUPP", "ENOSYS", "EPERM"]);
191
+ var TAKING_MS = 2e3;
192
+ function busyOnWindows(error) {
193
+ return process.platform === "win32" && WINDOWS_BUSY.has(error.code ?? "");
194
+ }
195
+ function alive(pid) {
196
+ if (!Number.isInteger(pid) || pid <= 0) return false;
197
+ try {
198
+ process.kill(pid, 0);
199
+ return true;
200
+ } catch (error) {
201
+ return error.code === "EPERM";
202
+ }
203
+ }
204
+ function ownerOf(file) {
205
+ try {
206
+ const text = readFileSync(file, "utf-8");
207
+ return /^\d+$/.test(text) ? Number(text) : Number.NaN;
208
+ } catch (error) {
209
+ if (error.code === "ENOENT") return null;
210
+ throw error;
211
+ }
212
+ }
213
+ function beingTaken(file) {
214
+ try {
215
+ const { size, mtimeMs } = statSync(file);
216
+ return size === 0 && Date.now() - mtimeMs < TAKING_MS;
217
+ } catch {
218
+ return false;
219
+ }
220
+ }
221
+ function deadHolder(file, pid) {
222
+ return !alive(pid) && !beingTaken(file);
223
+ }
224
+ function claim(file) {
225
+ const draft = `${file}-${process.pid}-${randomUUID2()}.tmp`;
226
+ writeFileSync(draft, String(process.pid));
227
+ try {
228
+ linkSync(draft, file);
229
+ return true;
230
+ } catch (error) {
231
+ const code = error.code ?? "";
232
+ if (code === "EEXIST") return false;
233
+ if (!NO_HARD_LINKS.has(code)) throw error;
234
+ return createExclusively(file);
235
+ } finally {
236
+ rmSync(draft, { force: true });
237
+ }
238
+ }
239
+ function createExclusively(file) {
240
+ try {
241
+ writeFileSync(file, String(process.pid), { flag: "wx" });
242
+ return true;
243
+ } catch (error) {
244
+ if (error.code !== "EEXIST") throw error;
245
+ return false;
246
+ }
247
+ }
248
+ function releaseOwn(file) {
249
+ if (ownerOf(file) === process.pid) rmSync(file, { force: true });
250
+ }
251
+ function evictDeadOwner(file) {
252
+ const evictor = `${file}.evict`;
253
+ if (!claim(evictor)) {
254
+ const holder = ownerOf(evictor);
255
+ if (holder !== null && deadHolder(evictor, holder)) rmSync(evictor, { force: true });
256
+ return false;
257
+ }
258
+ try {
259
+ const pid = ownerOf(file);
260
+ if (pid !== null && deadHolder(file, pid)) rmSync(file, { force: true });
261
+ return true;
262
+ } finally {
263
+ releaseOwn(evictor);
264
+ }
265
+ }
266
+ function advertise(urlFile, url) {
267
+ if (!url) return rmSync(urlFile, { force: true });
268
+ const draft = `${urlFile}-${process.pid}-${randomUUID2()}.tmp`;
269
+ writeFileSync(draft, JSON.stringify({ pid: process.pid, url }));
270
+ renameSync(draft, urlFile);
271
+ }
272
+ function servedBy(file, urlFile, pid) {
273
+ try {
274
+ const said = JSON.parse(readFileSync(urlFile, "utf-8"));
275
+ const live = said.pid === pid && alive(pid) && ownerOf(file) === pid;
276
+ return live && typeof said.url === "string" ? said.url : void 0;
277
+ } catch {
278
+ return void 0;
279
+ }
280
+ }
281
+ function attemptClaim(file, urlFile, deadline) {
282
+ if (claim(file)) return "claimed";
283
+ const pid = ownerOf(file);
284
+ if (pid === null) return "retry";
285
+ if (deadHolder(file, pid) && evictDeadOwner(file)) return "retry";
286
+ if (Date.now() >= deadline) throw new HistoryBusyError(pid, servedBy(file, urlFile, pid));
287
+ return "wait";
288
+ }
289
+ async function takeHistoryOwnership(home, waitMs, url) {
290
+ const file = join(home, "owner.pid");
291
+ const urlFile = join(home, "owner-url.json");
292
+ const deadline = Date.now() + waitMs;
293
+ mkdirSync(home, { recursive: true });
294
+ for (; ; ) {
295
+ let next = "wait";
296
+ try {
297
+ next = attemptClaim(file, urlFile, deadline);
298
+ } catch (error) {
299
+ if (!busyOnWindows(error) || Date.now() >= deadline) throw error;
300
+ }
301
+ if (next === "claimed") break;
302
+ if (next === "wait") await new Promise((settle) => setTimeout(settle, 50));
303
+ }
304
+ advertise(urlFile, url);
305
+ let held = true;
306
+ return () => {
307
+ if (held && servedBy(file, urlFile, process.pid)) rmSync(urlFile, { force: true });
308
+ if (held) releaseOwn(file);
309
+ held = false;
310
+ };
311
+ }
312
+ var manifest = ".hyperframes/render-outputs.json";
313
+ function readRenderOutputs(projectDir) {
314
+ const path = pinProjectRecord(projectDir, manifest);
315
+ if (!path) return [];
316
+ let text;
317
+ try {
318
+ text = readFileSync2(path, "utf8");
319
+ } catch (error) {
320
+ const code = error.code;
321
+ if (code === "ENOENT" || code === "ENOTDIR") return [];
322
+ throw error;
323
+ }
324
+ const value = JSON.parse(text);
325
+ if (value.version !== 1 || !Array.isArray(value.paths) || !value.paths.every(
326
+ (p) => typeof p === "string" && p !== "" && !isAbsolute(p) && !p.split(/[\\/]/).some((part) => part === ".." || part === ".")
327
+ ))
328
+ throw new Error("Invalid render output registry.");
329
+ return value.paths;
330
+ }
331
+ function readRenderOutputsOrNone(projectDir) {
332
+ try {
333
+ return readRenderOutputs(projectDir);
334
+ } catch {
335
+ return [];
336
+ }
337
+ }
338
+ function isRenderOutput(projectDir, path, outputs) {
339
+ const candidate = relative(resolve(projectDir), resolve(path)).split(sep).join("/");
340
+ return outputs.some((output) => candidate === output || candidate.startsWith(`${output}/`));
341
+ }
342
+ async function registerRenderOutput(projectDir, output) {
343
+ const path = relative(resolve(projectDir), resolve(output)).split(sep).join("/");
344
+ if (path === "") throw new Error("Render output cannot replace the project directory.");
345
+ if (isAbsolute(path) || path === ".." || path.startsWith("../")) return;
346
+ const registry = pinProjectRecord(projectDir, manifest);
347
+ const lock = pinProjectRecord(projectDir, ".hyperframes/render-outputs-lock");
348
+ if (!registry || !lock) throw new Error("Render output registry must stay inside the project.");
349
+ const release = await takeHistoryOwnership(lock, 5e3);
350
+ try {
351
+ const paths = [.../* @__PURE__ */ new Set([...readRenderOutputs(projectDir), path])].sort();
352
+ replaceFileAtomically(registry, JSON.stringify({ version: 1, paths }) + "\n", 420);
353
+ } finally {
354
+ release();
355
+ }
356
+ }
357
+ var SIGNATURE_TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
358
+ ".cjs",
359
+ ".css",
360
+ ".html",
361
+ ".js",
362
+ ".json",
363
+ ".jsx",
364
+ ".mjs",
365
+ ".svg",
366
+ ".ts",
367
+ ".tsx"
368
+ ]);
369
+ var MAX_SIGNATURE_TEXT_BYTES = 2e6;
370
+ var STUDIO_SIGNATURE_MANIFEST_PATHS = [
371
+ ".hyperframes/studio-manual-edits.json",
372
+ ".hyperframes/studio-motion.json"
373
+ ];
374
+ function affectsProjectSignature(projectDir, changedPath) {
375
+ const relativePath = relative2(resolve2(projectDir), resolve2(changedPath));
376
+ if (relativePath === "" || relativePath.startsWith("..") || isAbsolute2(relativePath) || isAtomicTempPath(relativePath)) {
377
+ return false;
378
+ }
379
+ if (isRenderOutput(projectDir, changedPath, readRenderOutputsOrNone(projectDir))) return false;
380
+ const segments = relativePath.split(sep2);
381
+ if (STUDIO_SIGNATURE_MANIFEST_PATHS.includes(segments.join("/"))) return true;
382
+ return !segments.some(isProjectIgnoredDir);
383
+ }
384
+ var projectSignatureCache = /* @__PURE__ */ new Map();
385
+ var COARSEST_FILE_TIME_TICK_MS = 2e3;
386
+ function isPathWithin(parentDir, childPath) {
387
+ const childRelativePath = relative2(parentDir, childPath);
388
+ return childRelativePath === "" || !childRelativePath.startsWith("..") && !isAbsolute2(childRelativePath);
389
+ }
390
+ function isTextContentEligible(file, size) {
391
+ return SIGNATURE_TEXT_EXTENSIONS.has(extname(file).toLowerCase()) && size <= MAX_SIGNATURE_TEXT_BYTES;
392
+ }
393
+ var isSkippedEntry = (entry) => isProjectIgnoredDir(entry) || isAtomicTempPath(entry);
394
+ function signatureEntryStat(projectDir, entry, file, outputs) {
395
+ if (isSkippedEntry(entry) || !isPathWithin(projectDir, file)) return null;
396
+ if (isRenderOutput(projectDir, file, outputs)) return null;
397
+ try {
398
+ const stat4 = lstatSync(file);
399
+ return stat4.isSymbolicLink() ? null : stat4;
400
+ } catch {
401
+ return null;
402
+ }
403
+ }
404
+ function collectProjectSignatureFiles(projectDir, dir, files, outputs = readRenderOutputsOrNone(projectDir)) {
405
+ let entries;
406
+ try {
407
+ entries = readdirSync(dir).sort();
408
+ } catch {
409
+ return;
410
+ }
411
+ for (const entry of entries) {
412
+ const file = resolve2(dir, entry);
413
+ const stat4 = signatureEntryStat(projectDir, entry, file, outputs);
414
+ if (!stat4) continue;
415
+ if (stat4.isDirectory()) {
416
+ collectProjectSignatureFiles(projectDir, file, files, outputs);
417
+ } else if (stat4.isFile()) {
418
+ files.push({
419
+ file,
420
+ mtimeMs: stat4.mtimeMs,
421
+ ctimeMs: stat4.ctimeMs,
422
+ size: stat4.size,
423
+ textContentEligible: isTextContentEligible(file, stat4.size)
424
+ });
425
+ }
426
+ }
427
+ }
428
+ function collectProjectSignatureManifestFiles(projectDir, files) {
429
+ const seen = new Set(files.map((entry) => entry.file));
430
+ for (const manifestPath of STUDIO_SIGNATURE_MANIFEST_PATHS) {
431
+ const file = resolve2(projectDir, manifestPath);
432
+ if (seen.has(file) || !isPathWithin(projectDir, file)) continue;
433
+ let stat4;
434
+ try {
435
+ stat4 = lstatSync(file);
436
+ } catch {
437
+ continue;
438
+ }
439
+ if (stat4.isSymbolicLink() || !stat4.isFile()) continue;
440
+ files.push({
441
+ file,
442
+ mtimeMs: stat4.mtimeMs,
443
+ ctimeMs: stat4.ctimeMs,
444
+ size: stat4.size,
445
+ textContentEligible: isTextContentEligible(file, stat4.size)
446
+ });
447
+ seen.add(file);
448
+ }
449
+ }
450
+ function createProjectFingerprint(projectDir, files) {
451
+ const hash = createHash2("sha256");
452
+ for (const entry of files) {
453
+ hash.update(relative2(projectDir, entry.file));
454
+ hash.update("\0");
455
+ hash.update(String(entry.size));
456
+ hash.update("\0");
457
+ hash.update(String(entry.mtimeMs));
458
+ hash.update("\0");
459
+ hash.update(String(entry.ctimeMs));
460
+ hash.update("\0");
461
+ hash.update(entry.textContentEligible ? "text" : "binary");
462
+ hash.update("\0");
463
+ }
464
+ return hash.digest("hex").slice(0, 24);
465
+ }
466
+ function collectProjectFiles(normalizedProjectDir) {
467
+ const collected = [];
468
+ collectProjectSignatureFiles(normalizedProjectDir, normalizedProjectDir, collected);
469
+ collectProjectSignatureManifestFiles(normalizedProjectDir, collected);
470
+ return collected;
471
+ }
472
+ function listProjectFiles(projectDir) {
473
+ const normalizedProjectDir = resolve2(projectDir);
474
+ return collectProjectFiles(normalizedProjectDir).map((entry) => ({
475
+ path: relative2(normalizedProjectDir, entry.file).split(sep2).join("/"),
476
+ size: entry.size,
477
+ mtimeMs: entry.mtimeMs,
478
+ ctimeMs: entry.ctimeMs
479
+ }));
480
+ }
481
+ function resolveProjectSignature(adapter, projectDir) {
482
+ return adapter.getProjectSignature?.(projectDir) ?? createProjectSignature(projectDir);
483
+ }
484
+ async function resolveProjectAndSignature(adapter, projectId) {
485
+ const project = await adapter.resolveProject(projectId);
486
+ if (!project) return null;
487
+ return { project, signature: resolveProjectSignature(adapter, project.dir) };
488
+ }
489
+ function createProjectSignature(projectDir, excluding = /* @__PURE__ */ new Set()) {
490
+ const normalizedProjectDir = resolve2(projectDir);
491
+ const signedAt = Date.now();
492
+ const collected = collectProjectFiles(normalizedProjectDir);
493
+ const files = collected.filter(
494
+ (entry) => !excluding.has(relative2(normalizedProjectDir, entry.file).split(sep2).join("/"))
495
+ );
496
+ files.sort((a, b) => a.file.localeCompare(b.file));
497
+ const fingerprint = createProjectFingerprint(normalizedProjectDir, files);
498
+ const cacheKey = excluding.size ? `${normalizedProjectDir}\0${createHash2("sha256").update([...excluding].sort().join("\0")).digest("hex")}` : normalizedProjectDir;
499
+ const cached = projectSignatureCache.get(cacheKey);
500
+ if (cached?.fingerprint === fingerprint) return cached.signature;
501
+ const hash = createHash2("sha256");
502
+ for (const entry of files) {
503
+ const relativePath = relative2(normalizedProjectDir, entry.file);
504
+ hash.update(relativePath);
505
+ hash.update("\0");
506
+ hash.update(String(entry.size));
507
+ hash.update("\0");
508
+ if (entry.textContentEligible) {
509
+ try {
510
+ hash.update(readFileSync3(entry.file));
511
+ } catch {
512
+ hash.update(String(entry.mtimeMs));
513
+ }
514
+ } else {
515
+ hash.update(String(entry.mtimeMs));
516
+ }
517
+ hash.update("\0");
518
+ }
519
+ const signature = hash.digest("hex").slice(0, 24);
520
+ const settledATickBeforeSigning = files.every(
521
+ (entry) => Math.max(entry.mtimeMs, entry.ctimeMs) < signedAt - COARSEST_FILE_TIME_TICK_MS
522
+ );
523
+ if (settledATickBeforeSigning) projectSignatureCache.set(cacheKey, { fingerprint, signature });
524
+ return signature;
525
+ }
526
+ var BLOB_HASH = /^[0-9a-f]{64}$/;
527
+ function blobPath(dir, hash) {
528
+ if (!BLOB_HASH.test(hash)) throw new Error("That is not a history blob.");
529
+ return join2(dir, hash.slice(0, 2), hash);
530
+ }
531
+ var readBlob = (dir, hash) => readFile(blobPath(dir, hash));
532
+ async function hashFile(path) {
533
+ const hash = createHash3("sha256");
534
+ for await (const chunk of createReadStream(path)) hash.update(chunk);
535
+ return hash.digest("hex");
536
+ }
537
+ async function cloneOrStream(from, to) {
538
+ try {
539
+ await copyFile(from, to, constants.COPYFILE_FICLONE_FORCE);
540
+ } catch {
541
+ const { mode } = await stat(from);
542
+ await pipeline(createReadStream(from), createWriteStream(to, { mode }));
543
+ }
544
+ }
545
+ async function cloneOrCopy(from, to, beforeReplace) {
546
+ await mkdir(dirname(to), { recursive: true });
547
+ const temp = atomicTempPath(to);
548
+ try {
549
+ await copyFile(from, temp, constants.COPYFILE_FICLONE);
550
+ beforeReplace?.();
551
+ renameSync2(temp, to);
552
+ } catch (error) {
553
+ await rm(temp, { force: true });
554
+ throw error;
555
+ }
556
+ }
557
+ var listDir = (path) => readdir(path, { withFileTypes: true }).catch((error) => {
558
+ if (error.code === "ENOENT") return [];
559
+ throw error;
560
+ });
561
+ async function openBlobStore(dir) {
562
+ const sizes = /* @__PURE__ */ new Map();
563
+ const asideDir = join2(dirname(dir), "aside");
564
+ const asideSizes = /* @__PURE__ */ new Map();
565
+ const holds = /* @__PURE__ */ new Map();
566
+ await mkdir(dir, { recursive: true });
567
+ for (const [root, found] of [
568
+ [dir, sizes],
569
+ [asideDir, asideSizes]
570
+ ])
571
+ for (const shard of await listDir(root)) {
572
+ if (!shard.isDirectory()) {
573
+ if (shard.name.startsWith("incoming-")) await rm(join2(root, shard.name), { force: true });
574
+ continue;
575
+ }
576
+ for (const hash of await listDir(join2(root, shard.name)))
577
+ found.set(hash.name, (await stat(join2(root, shard.name, hash.name))).size);
578
+ }
579
+ for (const hash of asideSizes.keys()) holds.set(hash, /* @__PURE__ */ new Set(["*"]));
580
+ const pathOf = (hash) => blobPath(sizes.has(hash) ? dir : asideDir, hash);
581
+ let total = [...sizes.values()].reduce((sum, size) => sum + size, 0);
582
+ const forget = async (hash) => {
583
+ await rm(blobPath(dir, hash), { force: true });
584
+ total -= sizes.get(hash) ?? 0;
585
+ sizes.delete(hash);
586
+ };
587
+ const forgetAside = async (hash) => {
588
+ await rm(blobPath(asideDir, hash), { force: true });
589
+ asideSizes.delete(hash);
590
+ holds.delete(hash);
591
+ };
592
+ return {
593
+ async put(absPath) {
594
+ const temp = join2(dir, `incoming-${randomUUID3()}`);
595
+ await mkdir(dir, { recursive: true });
596
+ await cloneOrStream(absPath, temp).catch(async (error) => {
597
+ await rm(temp, { force: true });
598
+ throw error;
599
+ });
600
+ const hash = await hashFile(temp);
601
+ if (sizes.has(hash)) {
602
+ await rm(temp, { force: true });
603
+ return hash;
604
+ }
605
+ await mkdir(dirname(blobPath(dir, hash)), { recursive: true });
606
+ const size = (await stat(temp)).size;
607
+ await rename(temp, blobPath(dir, hash));
608
+ sizes.set(hash, size);
609
+ total += size;
610
+ return hash;
611
+ },
612
+ reference: (absPath) => hashFile(absPath),
613
+ has: (hash) => sizes.has(hash) || asideSizes.has(hash),
614
+ read: (hash) => readFile(pathOf(hash)),
615
+ writeTo: async (hash, absPath, beforeReplace) => cloneOrCopy(pathOf(hash), absPath, beforeReplace),
616
+ async release(held) {
617
+ for (const [hash, path] of held) {
618
+ holds.get(hash)?.delete(path);
619
+ if (holds.get(hash)?.size === 0) await forgetAside(hash);
620
+ }
621
+ },
622
+ async keep(path, absPath, remove) {
623
+ const hash = await hashFile(absPath);
624
+ if (sizes.has(hash)) return;
625
+ if (asideSizes.has(hash)) {
626
+ holds.get(hash).add(path);
627
+ return;
628
+ }
629
+ const to = blobPath(asideDir, hash);
630
+ await mkdir(dirname(to), { recursive: true });
631
+ const moved = remove && await rename(absPath, to).then(
632
+ () => true,
633
+ () => false
634
+ );
635
+ if (!moved) {
636
+ const temp = join2(asideDir, `incoming-${randomUUID3()}`);
637
+ await cloneOrStream(absPath, temp).catch(async (error) => {
638
+ await rm(temp, { force: true });
639
+ throw error;
640
+ });
641
+ await rename(temp, to);
642
+ }
643
+ asideSizes.set(hash, (await stat(to)).size);
644
+ holds.set(hash, /* @__PURE__ */ new Set([path]));
645
+ },
646
+ bytes: () => total,
647
+ size: (hash) => sizes.get(hash) ?? 0,
648
+ async drop(hashes) {
649
+ for (const hash of hashes) if (sizes.has(hash)) await forget(hash);
650
+ },
651
+ async prune(keep) {
652
+ for (const hash of [...sizes.keys()]) if (!keep.has(hash)) await forget(hash);
653
+ for (const hash of [...asideSizes.keys()]) if (!keep.has(hash)) await forgetAside(hash);
654
+ }
655
+ };
656
+ }
657
+ var MEDIA_EXTENSIONS = /* @__PURE__ */ new Set([
658
+ ".mp4",
659
+ ".mov",
660
+ ".m4v",
661
+ ".webm",
662
+ ".mkv",
663
+ ".avi",
664
+ ".mxf",
665
+ ".m2ts",
666
+ ".mp3",
667
+ ".wav",
668
+ ".aac",
669
+ ".m4a",
670
+ ".flac",
671
+ ".ogg",
672
+ ".opus"
673
+ ]);
674
+ var COPY_NOTHING_ABOVE_BYTES = 64 * 1024 ** 2;
675
+ var isUncopied = (path, size) => size > COPY_NOTHING_ABOVE_BYTES || MEDIA_EXTENSIONS.has(extname2(path).toLowerCase());
676
+ function uncopiedOnlyHashes(log, sizeOf) {
677
+ const copied = /* @__PURE__ */ new Set();
678
+ const uncopied = /* @__PURE__ */ new Set();
679
+ const note = (path, hash) => {
680
+ if (hash) (isUncopied(path, sizeOf(hash)) ? uncopied : copied).add(hash);
681
+ };
682
+ for (const [path, hash] of log.baseline) note(path, hash);
683
+ for (const entry of log.entries)
684
+ for (const file of entry.files) {
685
+ note(file.path, file.before);
686
+ note(file.path, file.after);
687
+ }
688
+ return [...uncopied].filter((hash) => !copied.has(hash));
689
+ }
690
+ var ID_PATH = join3(".hyperframes", "history-id");
691
+ var ID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
692
+ var isHistoryId = (name) => ID_SHAPE.test(name);
693
+ function readId(projectDir) {
694
+ try {
695
+ const id = readFileSync4(join3(projectDir, ID_PATH), "utf-8").trim();
696
+ return ID_SHAPE.test(id) ? id : null;
697
+ } catch {
698
+ return null;
699
+ }
700
+ }
701
+ var sameFolder = (a, b) => a.ino === b.ino && a.birthtimeMs === b.birthtimeMs;
702
+ function isRecordedFolder(historyDir, folder) {
703
+ const was = readRecord(historyDir);
704
+ if (typeof was?.ino === "number")
705
+ return sameFolder({ ino: was.ino, birthtimeMs: was.born ?? NaN }, folder);
706
+ const unreadableSo0878KeptTheId = typeof was?.dir !== "string";
707
+ if (unreadableSo0878KeptTheId) return true;
708
+ return !isCopyOf0878Folder(was.dir, basename(historyDir), folder);
709
+ }
710
+ function readRecord(historyDir) {
711
+ try {
712
+ return JSON.parse(readFileSync4(join3(historyDir, "project.json"), "utf-8"));
713
+ } catch {
714
+ return null;
715
+ }
716
+ }
717
+ function isCopyOf0878Folder(recordedDir, id, folder) {
718
+ let there;
719
+ try {
720
+ there = statSync2(recordedDir, { throwIfNoEntry: false });
721
+ } catch {
722
+ return false;
723
+ }
724
+ return !!there && !sameFolder(there, folder) && readId(recordedDir) === id;
725
+ }
726
+ function projectHistoryId(projectDir, historyRoot) {
727
+ const dir = resolve3(projectDir);
728
+ const folder = statSync2(dir);
729
+ let id = readId(dir);
730
+ if (!id && existsSync(join3(dir, ID_PATH)))
731
+ throw new HistoryIdError(
732
+ `${join3(dir, ID_PATH)} holds no history id this version can read; move it aside to start anew.`
733
+ );
734
+ if (!id || existsSync(join3(historyRoot, id)) && !isRecordedFolder(join3(historyRoot, id), folder)) {
735
+ id = randomUUID4();
736
+ mkdirWithinProject(dir, join3(dir, ".hyperframes"));
737
+ writeFileSync2(join3(dir, ID_PATH), `${id}
738
+ `);
739
+ }
740
+ recordProject(join3(historyRoot, id), dir, folder);
741
+ return id;
742
+ }
743
+ var HistoryIdError = class extends Error {
744
+ constructor(message) {
745
+ super(message);
746
+ this.name = "HistoryIdError";
747
+ }
748
+ };
749
+ function recordProject(historyDir, dir, folder) {
750
+ const record = { dir, ino: folder.ino, born: folder.birthtimeMs, dev: folder.dev };
751
+ mkdirSync2(historyDir, { recursive: true });
752
+ replaceFileAtomically(join3(historyDir, "project.json"), JSON.stringify(record), 420);
753
+ }
754
+ var DAY_MS = 24 * 60 * 60 * 1e3;
755
+ var KEEP_GONE_PROJECT_HISTORY_MS = 14 * DAY_MS;
756
+ var PRUNED_PREFIX = ".pruned-";
757
+ var LAST_PRUNE_FILE = ".last-prune";
758
+ function listProjectHistories(historyRoot) {
759
+ return namesIn(historyRoot).filter(isHistoryId).flatMap((id) => {
760
+ const home = join4(historyRoot, id);
761
+ const projectDir = readRecord(home)?.dir;
762
+ if (typeof projectDir !== "string" || readId(projectDir) !== id) return [];
763
+ return [{ id, projectDir, lastUsedMs: lastUsed(home) }];
764
+ });
765
+ }
766
+ function namesIn(historyRoot) {
767
+ try {
768
+ return readdirSync2(historyRoot);
769
+ } catch (error) {
770
+ if (error.code === "ENOENT") return [];
771
+ throw error;
772
+ }
773
+ }
774
+ async function pruneGoneProjectHistories(historyRoot, options = {}) {
775
+ return (await pruneWithin(historyRoot, options)).pruned;
776
+ }
777
+ async function pruneWithin(historyRoot, {
778
+ now = Date.now(),
779
+ dryRun = false,
780
+ tempDir = tmpdir(),
781
+ onError = (error) => console.warn("[history] could not prune a history:", error),
782
+ budgetMs = Infinity,
783
+ measure = true
784
+ }) {
785
+ const started = performance.now();
786
+ const names = namesIn(historyRoot);
787
+ const pruned = [];
788
+ for (const name of names) {
789
+ if (performance.now() - started >= budgetMs) return { pruned, finished: false };
790
+ try {
791
+ const gone = await pruneEntry(historyRoot, name, { now, dryRun, tempDir, measure });
792
+ if (gone) pruned.push(gone);
793
+ } catch (error) {
794
+ onError(error);
795
+ }
796
+ }
797
+ return { pruned, finished: true };
798
+ }
799
+ async function pruneEntry(historyRoot, name, run) {
800
+ if (name.startsWith(PRUNED_PREFIX)) {
801
+ if (!run.dryRun) await rm2(join4(historyRoot, name), { recursive: true, force: true });
802
+ return null;
803
+ }
804
+ return isHistoryId(name) ? pruneOne(historyRoot, name, run) : null;
805
+ }
806
+ async function pruneOne(historyRoot, id, run) {
807
+ const home = join4(historyRoot, id);
808
+ if (abandonedProject(home, id, run.now, run.tempDir) === null) return null;
809
+ const release = await takeUnlessBusy(home);
810
+ if (!release) return null;
811
+ let taken;
812
+ try {
813
+ taken = recheckAndMoveAway(historyRoot, id, run);
814
+ } catch (error) {
815
+ release();
816
+ throw error;
817
+ }
818
+ if (!taken?.trash) release();
819
+ if (!taken) return null;
820
+ const bytes = await clearAway(taken.trash, home, run.measure);
821
+ return { id, projectDir: taken.projectDir, bytes };
822
+ }
823
+ async function clearAway(trash, home, measure) {
824
+ const bytes = measure ? await bytesIn(trash ?? home) : 0;
825
+ if (trash) await rm2(trash, { recursive: true, force: true });
826
+ return bytes;
827
+ }
828
+ async function takeUnlessBusy(home) {
829
+ try {
830
+ return await takeHistoryOwnership(home, 0);
831
+ } catch (error) {
832
+ if (error instanceof HistoryBusyError) return null;
833
+ throw error;
834
+ }
835
+ }
836
+ function recheckAndMoveAway(historyRoot, id, { now, dryRun, tempDir }) {
837
+ const home = join4(historyRoot, id);
838
+ const projectDir = abandonedProject(home, id, now, tempDir);
839
+ if (projectDir === null) return null;
840
+ if (dryRun) return { projectDir, trash: null };
841
+ const trash = join4(historyRoot, `${PRUNED_PREFIX}${id}-${randomUUID5()}`);
842
+ renameSync3(home, trash);
843
+ return { projectDir, trash };
844
+ }
845
+ function abandonedProject(home, id, now, tempDir) {
846
+ const record = readRecord(home);
847
+ const dir = record?.dir;
848
+ if (typeof dir !== "string") return null;
849
+ if (now - lastUsed(home) < KEEP_GONE_PROJECT_HISTORY_MS && !isSafePath(tempDir, dir)) return null;
850
+ return projectGone(dir, id, record?.dev, tempDir) ? dir : null;
851
+ }
852
+ function projectGone(dir, id, dev, tempDir) {
853
+ if (!statSync3(join4(dir, ID_PATH), { throwIfNoEntry: false }))
854
+ return diskStillHere(dir, dev, tempDir);
855
+ const there = readId(dir);
856
+ return there !== null && there !== id;
857
+ }
858
+ function diskStillHere(dir, dev, tempDir) {
859
+ let at = dir;
860
+ let stat4 = statSync3(at, { throwIfNoEntry: false });
861
+ while (!stat4) {
862
+ if (dirname2(at) === at) return false;
863
+ at = dirname2(at);
864
+ stat4 = statSync3(at, { throwIfNoEntry: false });
865
+ }
866
+ if (typeof dev === "number") return stat4.dev === dev;
867
+ return at === dir || isSafePath(tempDir, at);
868
+ }
869
+ function lastUsed(home) {
870
+ const mtime = (file) => statSync3(join4(home, file), { throwIfNoEntry: false })?.mtimeMs ?? 0;
871
+ return Math.max(mtime("project.json"), mtime("log.jsonl"));
872
+ }
873
+ async function bytesIn(dir) {
874
+ let total = 0;
875
+ for (const entry of await readdir2(dir, { withFileTypes: true })) {
876
+ const path = join4(dir, entry.name);
877
+ total += entry.isDirectory() ? await bytesIn(path) : (await lstat(path)).size;
878
+ }
879
+ return total;
880
+ }
881
+ function prunedWithinADay(stamp, now) {
882
+ try {
883
+ const last = Number(readFileSync5(stamp, "utf-8"));
884
+ return last <= now && now - last < DAY_MS;
885
+ } catch {
886
+ return false;
887
+ }
888
+ }
889
+ var pruning = /* @__PURE__ */ new Set();
890
+ function pruneGoneProjectHistoriesDaily(historyRoot, now, { onError, budgetMs } = {}) {
891
+ const stamp = join4(historyRoot, LAST_PRUNE_FILE);
892
+ if (pruning.has(historyRoot) || prunedWithinADay(stamp, now)) return;
893
+ pruning.add(historyRoot);
894
+ setImmediate(() => {
895
+ pruneWithin(historyRoot, { now, onError, budgetMs, measure: false }).then(({ finished }) => finished && writeFileSync3(stamp, String(now))).catch((error) => onError?.(error)).finally(() => pruning.delete(historyRoot));
896
+ });
897
+ }
898
+ var kinds = ["checks", "snapshots"];
899
+ var records = (state, kind) => [
900
+ ...Object.values(state[kind].revisions).flat(),
901
+ ...state[kind].unversioned
902
+ ];
903
+ var protectedRevisions = (state) => /* @__PURE__ */ new Set([state.currentRevision, ...state.renders?.retainedRevisions ?? []]);
904
+ function managedPath(projectDir, path, kind) {
905
+ const canonicalProject = realFilePath(resolve4(projectDir));
906
+ const canonicalPath = realFilePath(path);
907
+ const root = join5(canonicalProject, ".hyperframes", "artifacts", kind);
908
+ const rel = relative3(root, canonicalPath);
909
+ return isAbsolute3(path) && rel !== "" && rel !== ".." && !rel.startsWith(`..${sep3}`) && !isAbsolute3(rel) && pinProjectRecord(canonicalProject, relative3(canonicalProject, canonicalPath)) === canonicalPath;
910
+ }
911
+ function updateLatest(state) {
912
+ for (const kind of kinds) {
913
+ const available = records(state, kind).filter((record) => record.status !== "cleaned");
914
+ if (available.some(
915
+ (record) => record.id === state[kind].latest?.id && record.revision === state[kind].latest?.revision
916
+ ))
917
+ continue;
918
+ const latest = available.sort((a, b) => b.createdAt - a.createdAt)[0];
919
+ state[kind].latest = latest ? { revision: latest.revision, id: latest.id } : null;
920
+ }
921
+ }
922
+ function planArtifactCleanup(projectDir, state, render) {
923
+ const protectedIds = protectedRevisions(state);
924
+ const candidates = [];
925
+ const keepPaths = /* @__PURE__ */ new Set();
926
+ const removable = (record, path, kind) => Boolean(record.revision) && !protectedIds.has(record.revision) && record.createdAt <= render.completedAt && managedPath(projectDir, path, kind);
927
+ for (const kind of kinds) {
928
+ for (const record of records(state, kind)) {
929
+ const path = record.image ?? record.report;
930
+ if (!path || record.status === "cleaned") continue;
931
+ if (removable(record, path, kind)) candidates.push({ kind, record, path });
932
+ else keepPaths.add(realFilePath(path));
933
+ }
934
+ }
935
+ for (const { kind, record, path } of candidates) {
936
+ if (!keepPaths.has(realFilePath(path))) markCleaned(state, kind, record, path, render.id);
937
+ }
938
+ updateLatest(state);
939
+ }
940
+ function markCleaned(state, kind, record, path, renderId) {
941
+ (state.pendingArtifactCleanup ??= []).push({
942
+ path,
943
+ hash: record.contentHash,
944
+ revision: record.revision,
945
+ id: record.id,
946
+ kind
947
+ });
948
+ delete record.image;
949
+ delete record.report;
950
+ record.status = "cleaned";
951
+ record.cleanedAfterRender = renderId;
952
+ }
953
+ function restoreCleaned(state, item) {
954
+ const record = state[item.kind].revisions[item.revision]?.find(
955
+ (candidate) => candidate.id === item.id
956
+ );
957
+ if (record?.status !== "cleaned") return;
958
+ if (item.kind === "checks") record.report = item.path;
959
+ else record.image = item.path;
960
+ delete record.status;
961
+ delete record.cleanedAfterRender;
962
+ }
963
+ function stillPendingAfterUnlink(item) {
964
+ let descriptor;
965
+ try {
966
+ descriptor = openSync(item.path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
967
+ const stat4 = fstatSync(descriptor);
968
+ if (!stat4.isFile() || stat4.isSymbolicLink()) return false;
969
+ const hash = createHash4("sha256").update(readFileSync6(descriptor)).digest("hex");
970
+ if (hash !== item.hash) return false;
971
+ const current = lstatSync2(item.path);
972
+ if (current.dev !== stat4.dev || current.ino !== stat4.ino) return false;
973
+ unlinkSync(item.path);
974
+ return false;
975
+ } catch (error) {
976
+ return error.code !== "ENOENT";
977
+ } finally {
978
+ if (descriptor !== void 0) closeSync(descriptor);
979
+ }
980
+ }
981
+ function finishArtifactCleanup(projectDir, state) {
982
+ const protectedIds = protectedRevisions(state);
983
+ const referenced = new Set(
984
+ kinds.flatMap(
985
+ (kind) => records(state, kind).flatMap((record) => record.image ?? record.report ?? []).map(realFilePath)
986
+ )
987
+ );
988
+ state.pendingArtifactCleanup = (state.pendingArtifactCleanup ?? []).filter((item) => {
989
+ if (!managedPath(projectDir, item.path, item.kind)) return false;
990
+ const canonicalPath = realFilePath(item.path);
991
+ if (protectedIds.has(item.revision)) {
992
+ restoreCleaned(state, item);
993
+ return false;
994
+ }
995
+ if (state.pendingChanges) return true;
996
+ if (referenced.has(canonicalPath)) return false;
997
+ return stillPendingAfterUnlink({ ...item, path: canonicalPath });
998
+ });
999
+ updateLatest(state);
1000
+ }
1001
+ var SNAPSHOT_RUNS_TO_KEEP = 10;
1002
+ var SNAPSHOT_GRACE_MS = 24 * 60 * 60 * 1e3;
1003
+ var OWNER = ".capture-owner.json";
1004
+ var ownedMarkers = /* @__PURE__ */ new Map();
1005
+ var UUID = /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/i;
1006
+ var rootOf = (project) => join6(resolve5(project), ".hyperframes", "artifacts", "snapshots");
1007
+ function ordinaryPath(project, path) {
1008
+ const root = resolve5(project);
1009
+ const rel = relative4(root, path);
1010
+ if (rel === ".." || rel.startsWith(`..${sep4}`)) return false;
1011
+ let current = root;
1012
+ for (const part of rel.split(sep4).filter(Boolean)) {
1013
+ current = join6(current, part);
1014
+ try {
1015
+ if (lstatSync3(current).isSymbolicLink()) return false;
1016
+ } catch {
1017
+ return false;
1018
+ }
1019
+ }
1020
+ return true;
1021
+ }
1022
+ function createSnapshotParents(project, path) {
1023
+ let parent = resolve5(project);
1024
+ for (const part of relative4(parent, path).split(sep4).filter(Boolean)) {
1025
+ parent = join6(parent, part);
1026
+ try {
1027
+ mkdirSync3(parent);
1028
+ } catch (error) {
1029
+ if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
1030
+ }
1031
+ if (!ordinaryPath(project, parent)) throw new Error("Snapshot storage must not use symlinks.");
1032
+ }
1033
+ }
1034
+ function ownSnapshotRun(project, path) {
1035
+ const root = rootOf(project);
1036
+ if (dirname3(path) !== root || !UUID.test(path.slice(root.length + 1))) return;
1037
+ createSnapshotParents(project, root);
1038
+ mkdirSync3(path);
1039
+ const marker = join6(path, OWNER);
1040
+ const fd = openSync2(marker, "wx");
1041
+ try {
1042
+ writeFileSync4(fd, JSON.stringify({ version: 1, pid: process.pid }));
1043
+ ownedMarkers.set(marker, fstatSync2(fd));
1044
+ } finally {
1045
+ closeSync2(fd);
1046
+ }
1047
+ }
1048
+ function missingFile(error) {
1049
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
1050
+ }
1051
+ function retainedOutput(path) {
1052
+ try {
1053
+ lstatSync3(join6(path, ".user-output"));
1054
+ return true;
1055
+ } catch (error) {
1056
+ return !missingFile(error);
1057
+ }
1058
+ }
1059
+ function processIsAlive(pid) {
1060
+ try {
1061
+ process.kill(pid, 0);
1062
+ return true;
1063
+ } catch (error) {
1064
+ return !(error instanceof Error && "code" in error && error.code === "ESRCH");
1065
+ }
1066
+ }
1067
+ function ownerPid(owner) {
1068
+ if (!owner || typeof owner !== "object" || !("pid" in owner)) return null;
1069
+ const pid = owner.pid;
1070
+ return typeof pid === "number" && Number.isSafeInteger(pid) && pid > 0 ? pid : null;
1071
+ }
1072
+ function ordinaryOwner(stat4) {
1073
+ return stat4.isFile() && stat4.nlink === 1;
1074
+ }
1075
+ function sameOwnerFile(before, current) {
1076
+ return current.dev === before.dev && current.ino === before.ino;
1077
+ }
1078
+ function openOwner(marker, access) {
1079
+ const fd = openSync2(marker, access | (constants3.O_NOFOLLOW ?? 0) | (constants3.O_NONBLOCK ?? 0));
1080
+ try {
1081
+ const current = fstatSync2(fd);
1082
+ const named = lstatSync3(marker);
1083
+ if (!ordinaryOwner(current) || !ordinaryOwner(named) || !sameOwnerFile(named, current))
1084
+ throw new Error("Snapshot owner changed while opening.");
1085
+ return fd;
1086
+ } catch (error) {
1087
+ closeSync2(fd);
1088
+ throw error;
1089
+ }
1090
+ }
1091
+ function readOwner(marker) {
1092
+ const fd = openOwner(marker, constants3.O_RDONLY);
1093
+ try {
1094
+ return JSON.parse(readFileSync7(fd, "utf8"));
1095
+ } finally {
1096
+ closeSync2(fd);
1097
+ }
1098
+ }
1099
+ function releasedOwner(owner) {
1100
+ return !!owner && typeof owner === "object" && "released" in owner && owner.released === true && ownerPid(owner) !== null;
1101
+ }
1102
+ function liveOwner(path) {
1103
+ if (retainedOutput(path)) return true;
1104
+ try {
1105
+ const marker = join6(path, OWNER);
1106
+ const owner = readOwner(marker);
1107
+ if (releasedOwner(owner)) return false;
1108
+ const pid = ownerPid(owner);
1109
+ return pid === null || processIsAlive(pid);
1110
+ } catch (error) {
1111
+ return !missingFile(error);
1112
+ }
1113
+ }
1114
+ function recordsOf(state) {
1115
+ return [...Object.values(state.snapshots.revisions).flat(), ...state.snapshots.unversioned];
1116
+ }
1117
+ function containingRun(root, path) {
1118
+ if (!path) return null;
1119
+ const name = relative4(root, resolve5(path)).split(sep4)[0];
1120
+ return name && UUID.test(name) ? join6(root, name) : null;
1121
+ }
1122
+ function keepContainingRun(keep, root, path) {
1123
+ const run = containingRun(root, path);
1124
+ if (run) keep.add(run);
1125
+ }
1126
+ function protectedRuns(root, state, records2) {
1127
+ const keep = /* @__PURE__ */ new Set();
1128
+ for (const revision of [state.currentRevision, ...state.renders?.retainedRevisions ?? []]) {
1129
+ const latest2 = records2.filter((record) => record.revision === revision && record.status !== "cleaned").sort((a, b) => b.createdAt - a.createdAt)[0];
1130
+ keepContainingRun(keep, root, latest2?.image);
1131
+ }
1132
+ const latest = records2.find((record) => record.id === state.snapshots.latest?.id);
1133
+ keepContainingRun(keep, root, latest?.image);
1134
+ for (const item of state.pendingArtifactCleanup ?? []) keepContainingRun(keep, root, item.path);
1135
+ return keep;
1136
+ }
1137
+ function runsOnDisk(root) {
1138
+ let entries;
1139
+ try {
1140
+ entries = readdirSync3(root, { withFileTypes: true });
1141
+ } catch {
1142
+ return [];
1143
+ }
1144
+ return entries.filter((entry) => entry.isDirectory() && UUID.test(entry.name)).flatMap((entry) => {
1145
+ const path = join6(root, entry.name);
1146
+ try {
1147
+ const stat4 = lstatSync3(path);
1148
+ return [{ path, stat: stat4 }];
1149
+ } catch {
1150
+ return [];
1151
+ }
1152
+ }).sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs || a.path.localeCompare(b.path));
1153
+ }
1154
+ function planSnapshotCleanup(project, state, now = Date.now()) {
1155
+ const root = rootOf(project);
1156
+ if (!ordinaryPath(project, root)) return [];
1157
+ const records2 = recordsOf(state);
1158
+ const keep = protectedRuns(root, state, records2);
1159
+ const runs = runsOnDisk(root);
1160
+ for (const run of runs.slice(0, SNAPSHOT_RUNS_TO_KEEP)) keep.add(run.path);
1161
+ const pending = [];
1162
+ for (const { path, stat: stat4 } of runs) {
1163
+ if (keep.has(path) || now - stat4.mtimeMs < SNAPSHOT_GRACE_MS || liveOwner(path)) continue;
1164
+ try {
1165
+ const run = { path, dev: stat4.dev, ino: stat4.ino, mtimeMs: stat4.mtimeMs };
1166
+ if (!unchangedRun(project, run)) continue;
1167
+ pending.push(run);
1168
+ retireReferences(records2, root, path);
1169
+ } catch {
1170
+ }
1171
+ }
1172
+ return pending;
1173
+ }
1174
+ function retireReferences(records2, root, path) {
1175
+ for (const record of records2) {
1176
+ if (containingRun(root, record.image) !== path) continue;
1177
+ delete record.image;
1178
+ record.status = "cleaned";
1179
+ }
1180
+ }
1181
+ function unchangedRun(project, run) {
1182
+ if (!ordinaryPath(project, run.path)) return false;
1183
+ const current = lstatSync3(run.path);
1184
+ return current.isDirectory() && current.dev === run.dev && current.ino === run.ino && current.mtimeMs === run.mtimeMs;
1185
+ }
1186
+ function finishSnapshotCleanup(project, runs) {
1187
+ for (const run of runs) {
1188
+ try {
1189
+ if (!unchangedRun(project, run) || liveOwner(run.path)) continue;
1190
+ rmSync2(run.path, { recursive: true });
1191
+ } catch {
1192
+ }
1193
+ }
1194
+ }
1195
+ function releaseSnapshotRun(project, path) {
1196
+ const marker = join6(path, OWNER);
1197
+ const expected = ownedMarkers.get(marker);
1198
+ if (!expected) return;
1199
+ try {
1200
+ if (dirname3(path) !== rootOf(project) || !ordinaryPath(project, path)) return;
1201
+ const fd = openOwner(marker, constants3.O_RDWR);
1202
+ try {
1203
+ if (!sameOwnerFile(expected, fstatSync2(fd))) return;
1204
+ const owner = JSON.parse(readFileSync7(fd, "utf8"));
1205
+ if (ownerPid(owner) !== process.pid) return;
1206
+ ftruncateSync(fd, 0);
1207
+ writeSync(fd, JSON.stringify({ version: 1, pid: process.pid, released: true }), 0, "utf8");
1208
+ } finally {
1209
+ closeSync2(fd);
1210
+ }
1211
+ } catch {
1212
+ } finally {
1213
+ ownedMarkers.delete(marker);
1214
+ }
1215
+ }
1216
+ function retainSnapshotOutput(project, path) {
1217
+ const root = rootOf(project);
1218
+ const run = containingRun(root, path);
1219
+ if (!run) return;
1220
+ createSnapshotParents(project, resolve5(path));
1221
+ try {
1222
+ writeFileSync4(
1223
+ join6(run, ".user-output"),
1224
+ "Explicit --output; retained until manual cleanup.\n",
1225
+ { flag: "wx" }
1226
+ );
1227
+ } catch (error) {
1228
+ if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
1229
+ }
1230
+ }
1231
+ function statePath(dir) {
1232
+ const path = pinProjectRecord(dir, ".hyperframes/video-state.json");
1233
+ if (!path) throw new Error("Video state must stay inside the project.");
1234
+ return path;
1235
+ }
1236
+ var collection = () => ({ latest: null, revisions: {}, unversioned: [] });
1237
+ var emptyState = () => ({
1238
+ version: 1,
1239
+ historyId: null,
1240
+ currentRevision: null,
1241
+ pendingChanges: true,
1242
+ files: {},
1243
+ revisions: {},
1244
+ checks: collection(),
1245
+ snapshots: collection()
1246
+ });
1247
+ function readState(dir) {
1248
+ if (!existsSync2(statePath(dir))) return emptyState();
1249
+ const state = JSON.parse(readFileSync8(statePath(dir), "utf8"));
1250
+ if (state.version !== 1 || !state.checks || !state.snapshots || !state.revisions || !state.files)
1251
+ throw new Error("Unsupported video-state schema.");
1252
+ return state;
1253
+ }
1254
+ async function updateState(dir, update) {
1255
+ const lock = pinProjectRecord(dir, ".hyperframes/video-state-lock");
1256
+ if (!lock) throw new Error("Video state lock must stay inside the project.");
1257
+ const release = await takeHistoryOwnership(lock, 5e3);
1258
+ try {
1259
+ let state;
1260
+ try {
1261
+ state = readState(dir);
1262
+ } catch (error) {
1263
+ if (!(error instanceof SyntaxError)) throw error;
1264
+ renameSync4(statePath(dir), `${statePath(dir)}.invalid-${randomUUID6()}`);
1265
+ state = emptyState();
1266
+ }
1267
+ update(state);
1268
+ replaceFileAtomically(statePath(dir), JSON.stringify(state, null, 2) + "\n", 420);
1269
+ } finally {
1270
+ release();
1271
+ }
1272
+ }
1273
+ async function writeHistoryState(projectDir, historyId, historyDirectory, entries, currentFiles, committedFiles) {
1274
+ const blob = (hash) => hash === null ? null : resolve6(historyDirectory, "blobs", hash.slice(0, 2), hash);
1275
+ await updateState(projectDir, (state) => {
1276
+ if (state.historyId && state.historyId !== historyId) {
1277
+ state.checks = collection();
1278
+ state.snapshots = collection();
1279
+ }
1280
+ state.historyId = historyId;
1281
+ state.currentRevision = entries.at(-1)?.id ?? null;
1282
+ state.pendingChanges = currentFiles.size !== committedFiles.size || [...currentFiles].some(([path, hash]) => committedFiles.get(path) !== hash);
1283
+ state.files = Object.fromEntries(
1284
+ [...currentFiles].map(([path, hash]) => [path, { hash, content: blob(hash) }])
1285
+ );
1286
+ state.revisions = Object.fromEntries(
1287
+ entries.map((entry) => [entry.id, revisionState(entry, blob)])
1288
+ );
1289
+ dropArtifactsOfGoneRevisions(state);
1290
+ });
1291
+ }
1292
+ function revisionState(entry, blob) {
1293
+ const targets = entry.targets?.filter(
1294
+ (target) => entry.files.some((file) => file.path === target.file)
1295
+ );
1296
+ return {
1297
+ actor: entry.who,
1298
+ ...targets?.length && { targets },
1299
+ label: entry.label,
1300
+ startedAt: entry.startedAt,
1301
+ endedAt: entry.endedAt,
1302
+ ...entry.undoes && { undoes: entry.undoes },
1303
+ ...entry.restoredTo && { restoredTo: entry.restoredTo },
1304
+ files: Object.fromEntries(
1305
+ entry.files.map((file) => [
1306
+ file.path,
1307
+ { before: blob(file.before), after: blob(file.after) }
1308
+ ])
1309
+ )
1310
+ };
1311
+ }
1312
+ function dropArtifactsOfGoneRevisions(state) {
1313
+ const kept = (revision) => Boolean(state.revisions[revision]) || Boolean(state.renders?.retainedRevisions?.includes(revision));
1314
+ for (const artifacts of [state.checks, state.snapshots]) {
1315
+ for (const revision of Object.keys(artifacts.revisions)) {
1316
+ if (!kept(revision)) delete artifacts.revisions[revision];
1317
+ }
1318
+ if (artifacts.latest?.revision && !state.revisions[artifacts.latest.revision])
1319
+ artifacts.latest = null;
1320
+ }
1321
+ }
1322
+ async function fileHashes(dir) {
1323
+ const files = {};
1324
+ for (const { path } of listProjectFiles(dir).sort((a, b) => a.path.localeCompare(b.path))) {
1325
+ const hash = createHash5("sha256");
1326
+ for await (const chunk of createReadStream2(join7(dir, path))) hash.update(chunk);
1327
+ files[path] = hash.digest("hex");
1328
+ }
1329
+ return files;
1330
+ }
1331
+ async function beginArtifactCapture(projectDir) {
1332
+ const files = await fileHashes(projectDir);
1333
+ const state = readState(projectDir);
1334
+ 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]);
1335
+ return {
1336
+ revision: matches ? state.currentRevision : null,
1337
+ fingerprint: createHash5("sha256").update(JSON.stringify(files)).digest("hex")
1338
+ };
1339
+ }
1340
+ async function recordArtifact(projectDir, capture, artifact) {
1341
+ return recordArtifacts(projectDir, capture, [artifact]);
1342
+ }
1343
+ async function recordArtifacts(projectDir, capture, artifactsToRecord) {
1344
+ const records2 = [];
1345
+ const hashes = /* @__PURE__ */ new Map();
1346
+ for (const artifact of artifactsToRecord) {
1347
+ const path = artifact.kind === "check" ? artifact.report : artifact.image;
1348
+ if (!path || !isAbsolute4(path)) throw new Error("Artifact needs an absolute output path.");
1349
+ hashes.set(artifact.id, createHash5("sha256").update(readFileSync8(path)).digest("hex"));
1350
+ }
1351
+ const after = await beginArtifactCapture(projectDir);
1352
+ const unchanged = after.fingerprint === capture.fingerprint;
1353
+ let cleanup = [];
1354
+ await updateState(projectDir, (state) => {
1355
+ const revision = unchanged ? knownRevision(state, capture.revision) : null;
1356
+ const attribution = attributionOf(unchanged, revision);
1357
+ for (const artifact of artifactsToRecord) {
1358
+ const { kind, ...fields } = artifact;
1359
+ const record = {
1360
+ ...fields,
1361
+ createdAt: Date.now(),
1362
+ contentHash: hashes.get(artifact.id),
1363
+ revision,
1364
+ inputFingerprint: capture.fingerprint,
1365
+ inputScope: "local-project-files",
1366
+ attribution
1367
+ };
1368
+ const artifacts = kind === "check" ? state.checks : state.snapshots;
1369
+ dropSupersededArtifacts(artifacts, artifact);
1370
+ const list = revision ? artifacts.revisions[revision] ??= [] : artifacts.unversioned;
1371
+ list.push(record);
1372
+ records2.push(record);
1373
+ artifacts.latest = { revision, id: record.id };
1374
+ }
1375
+ cleanup = planSnapshotCleanup(projectDir, state);
1376
+ });
1377
+ finishSnapshotCleanup(projectDir, cleanup);
1378
+ return records2;
1379
+ }
1380
+ function knownRevision(state, revision) {
1381
+ return revision && state.revisions[revision] ? revision : null;
1382
+ }
1383
+ function attributionOf(unchanged, revision) {
1384
+ if (!unchanged) return "inputs-changed";
1385
+ return revision ? "recorded-revision" : "unversioned";
1386
+ }
1387
+ function supersedes(artifact, previous) {
1388
+ if (previous.status !== "cleaned" && !existsSync2(previous.image ?? previous.report ?? ""))
1389
+ return true;
1390
+ if (previous.id === artifact.id) return true;
1391
+ if (artifact.image && previous.image === artifact.image) return true;
1392
+ return Boolean(artifact.report && previous.report === artifact.report);
1393
+ }
1394
+ function dropSupersededArtifacts(artifacts, artifact) {
1395
+ for (const list of [...Object.values(artifacts.revisions), artifacts.unversioned]) {
1396
+ for (let i = list.length - 1; i >= 0; i--) {
1397
+ if (supersedes(artifact, list[i])) list.splice(i, 1);
1398
+ }
1399
+ }
1400
+ }
1401
+ function listsOf(artifacts) {
1402
+ return [...Object.values(artifacts.revisions), artifacts.unversioned];
1403
+ }
1404
+ async function forgetRecordedArtifacts(projectDir, paths) {
1405
+ const gone = new Set(paths.map((path) => resolve6(path)));
1406
+ if (gone.size === 0) return;
1407
+ await updateState(projectDir, (state) => {
1408
+ for (const artifacts of [state.checks, state.snapshots]) {
1409
+ for (const list of listsOf(artifacts)) {
1410
+ for (let i = list.length - 1; i >= 0; i--) {
1411
+ const record = list[i];
1412
+ const path = record.image ?? record.report;
1413
+ if (path && gone.has(resolve6(path))) list.splice(i, 1);
1414
+ }
1415
+ }
1416
+ if (artifacts.latest && !listsOf(artifacts).some(
1417
+ (list) => list.some((record) => record.id === artifacts.latest?.id)
1418
+ )) {
1419
+ artifacts.latest = null;
1420
+ }
1421
+ }
1422
+ });
1423
+ }
1424
+ function artifactDirectory(projectDir, kind) {
1425
+ const path = join7(resolve6(projectDir), ".hyperframes", "artifacts", kind, randomUUID6());
1426
+ if (kind === "snapshots") ownSnapshotRun(projectDir, path);
1427
+ return path;
1428
+ }
1429
+ function retainRevision(retained, revision) {
1430
+ if (!revision) return [...retained];
1431
+ return [revision, ...retained.filter((id) => id !== revision)].slice(0, 2);
1432
+ }
1433
+ async function recordRenderSuccess(projectDir, output, status = "completed", capture) {
1434
+ const outputFile = statSync4(output);
1435
+ if (!outputFile.isFile() && !outputFile.isDirectory() || outputFile.isFile() && outputFile.size === 0)
1436
+ throw new Error("Render output is missing or empty.");
1437
+ const after = capture ? await beginArtifactCapture(projectDir) : null;
1438
+ const record = {
1439
+ id: randomUUID6(),
1440
+ completedAt: Date.now(),
1441
+ output: resolve6(output),
1442
+ status
1443
+ };
1444
+ const unchanged = Boolean(capture && after?.fingerprint === capture.fingerprint);
1445
+ await updateState(projectDir, (state) => {
1446
+ record.revision = unchanged ? knownRevision(state, capture?.revision) : null;
1447
+ state.renders = {
1448
+ latest: record,
1449
+ retainedRevisions: retainRevision(state.renders?.retainedRevisions ?? [], record.revision)
1450
+ };
1451
+ if (outputFile.isFile() && record.revision && !state.pendingChanges)
1452
+ planArtifactCleanup(projectDir, state, record);
1453
+ });
1454
+ await updateState(projectDir, (state) => {
1455
+ finishArtifactCleanup(projectDir, state);
1456
+ });
1457
+ return record;
1458
+ }
1459
+ async function finishSnapshotRun(projectDir, path) {
1460
+ releaseSnapshotRun(projectDir, path);
1461
+ let cleanup = [];
1462
+ await updateState(projectDir, (state) => {
1463
+ cleanup = planSnapshotCleanup(projectDir, state);
1464
+ });
1465
+ finishSnapshotCleanup(projectDir, cleanup);
1466
+ }
1467
+ var START = "start";
1468
+ function parseRecord(line) {
1469
+ try {
1470
+ return JSON.parse(line);
1471
+ } catch {
1472
+ return null;
1473
+ }
1474
+ }
1475
+ function readLog(file, onUnreadable) {
1476
+ let text;
1477
+ try {
1478
+ text = readFileSync9(file, "utf-8");
1479
+ } catch {
1480
+ return null;
1481
+ }
1482
+ const log = { baseline: /* @__PURE__ */ new Map(), entries: [], pins: /* @__PURE__ */ new Set() };
1483
+ const lines = text.split("\n");
1484
+ lines.forEach((line, index) => {
1485
+ const record = line ? parseRecord(line) : null;
1486
+ if (record) applyRecord(log, record);
1487
+ else if (line && index < lines.length - 1) onUnreadable(index + 1);
1488
+ });
1489
+ return log;
1490
+ }
1491
+ function applyRecord(log, record) {
1492
+ if (record.type === "baseline") {
1493
+ log.baseline = new Map(Object.entries(record.files));
1494
+ log.keepsLedger = record.keepsLedger === true;
1495
+ } else if (record.type === "entry") log.entries.push(record.entry);
1496
+ else if (record.pinned) log.pins.add(record.id);
1497
+ else log.pins.delete(record.id);
1498
+ }
1499
+ function saveRecord(file, log, record) {
1500
+ let fd;
1501
+ try {
1502
+ fd = openSync3(file, constants4.O_WRONLY | constants4.O_APPEND);
1503
+ } catch (error) {
1504
+ if (error.code !== "ENOENT") throw error;
1505
+ return writeLog(file, log);
1506
+ }
1507
+ try {
1508
+ writeSync2(fd, `${JSON.stringify(record)}
1509
+ `);
1510
+ } finally {
1511
+ closeSync3(fd);
1512
+ }
1513
+ }
1514
+ function baselineRecord(log) {
1515
+ return {
1516
+ type: "baseline",
1517
+ files: Object.fromEntries(log.baseline),
1518
+ ...log.keepsLedger && { keepsLedger: true }
1519
+ };
1520
+ }
1521
+ function writeLog(file, log) {
1522
+ const records2 = [
1523
+ baselineRecord(log),
1524
+ ...log.entries.map((entry) => ({ type: "entry", entry })),
1525
+ ...[...log.pins].map((id) => ({ type: "pin", id, pinned: true }))
1526
+ ];
1527
+ mkdirSync4(dirname4(file), { recursive: true });
1528
+ replaceFileAtomically(file, records2.map((r) => `${JSON.stringify(r)}
1529
+ `).join(""), 420);
1530
+ }
1531
+ function applyEntry(manifest2, entry, side) {
1532
+ for (const file of entry.files) {
1533
+ const hash = file[side];
1534
+ if (hash === null) manifest2.delete(file.path);
1535
+ else manifest2.set(file.path, hash);
1536
+ }
1537
+ }
1538
+ function manifestAt(log, point) {
1539
+ return point === START ? new Map(log.baseline) : manifestAround(log, point, "after");
1540
+ }
1541
+ function manifestAround(log, id, side) {
1542
+ const manifest2 = new Map(log.baseline);
1543
+ for (const entry of log.entries) {
1544
+ applyEntry(manifest2, entry, entry.id === id ? side : "after");
1545
+ if (entry.id === id) return manifest2;
1546
+ }
1547
+ return null;
1548
+ }
1549
+ function undoneIds(entries) {
1550
+ const undone = /* @__PURE__ */ new Set();
1551
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
1552
+ const entry = entries[i];
1553
+ if (entry.undoes && !undone.has(entry.id)) undone.add(entry.undoes);
1554
+ }
1555
+ return undone;
1556
+ }
1557
+ function stepTarget(entries, direction, mine) {
1558
+ const undone = undoneIds(entries);
1559
+ const byId = new Map(entries.map((entry) => [entry.id, entry]));
1560
+ const owns = ownerOf2(entries, byId, mine);
1561
+ const newestFirst = [...entries].reverse();
1562
+ if (direction === "back")
1563
+ return newestFirst.find((entry) => !entry.undoes && !undone.has(entry.id) && owns(entry));
1564
+ for (const entry of newestFirst) {
1565
+ if (!entry.undoes) {
1566
+ if (owns(entry)) return void 0;
1567
+ continue;
1568
+ }
1569
+ const target = byId.get(entry.undoes);
1570
+ if (mine(entry) && !undone.has(entry.id) && target && !target.undoes) return entry;
1571
+ }
1572
+ return void 0;
1573
+ }
1574
+ function ownerOf2(entries, byId, mine) {
1575
+ const redoneBy = /* @__PURE__ */ new Map();
1576
+ for (const entry of entries) {
1577
+ const target = entry.undoes ? byId.get(entry.undoes) : void 0;
1578
+ if (target?.undoes) redoneBy.set(target.undoes, entry);
1579
+ }
1580
+ return (entry) => mine(entry) || mine(redoneBy.get(entry.id) ?? entry);
1581
+ }
1582
+ function foldOldest(log) {
1583
+ const oldest = log.entries[0];
1584
+ if (!oldest || log.pins.has(oldest.id)) return false;
1585
+ applyEntry(log.baseline, oldest, "after");
1586
+ log.entries.shift();
1587
+ return true;
1588
+ }
1589
+ function referencedHashes(log, current) {
1590
+ const keep = /* @__PURE__ */ new Set([...log.baseline.values(), ...current.values()]);
1591
+ for (const entry of log.entries)
1592
+ for (const file of entry.files) {
1593
+ if (file.before) keep.add(file.before);
1594
+ if (file.after) keep.add(file.after);
1595
+ }
1596
+ return keep;
1597
+ }
1598
+ var DEFAULT_HISTORY_ROOT = join8(homedir(), ".cache", "hyperframes", "history");
1599
+ var MAX_WINDOW_IDLE_MS = 10 * 6e4;
1600
+ var UNDO_MODES = ["just-this", "back-to-before", "keep-later-edits"];
1601
+ var OUTSIDE = { kind: "outside", name: "Outside" };
1602
+ var OUTSIDE_LABEL = "Changed outside the app";
1603
+ var RACY_MS = 2e3;
1604
+ var statKey = (file, sweptAt) => sweptAt - Math.max(file.mtimeMs, file.ctimeMs) < RACY_MS ? "" : `${file.size}:${file.mtimeMs}:${file.ctimeMs}`;
1605
+ var MEDIA_LEDGER = ".media/manifest.jsonl";
1606
+ var KEPT_HIDDEN_PATHS = /* @__PURE__ */ new Set([...STUDIO_SIGNATURE_MANIFEST_PATHS, MEDIA_LEDGER]);
1607
+ function isHistoryPath(path) {
1608
+ if (KEPT_HIDDEN_PATHS.has(path)) return true;
1609
+ return !path.split("/").some((segment) => segment.startsWith("."));
1610
+ }
1611
+ var logFileIn = (home) => join8(home, "log.jsonl");
1612
+ function listOf(log) {
1613
+ const undone = undoneIds(log.entries);
1614
+ return log.entries.map((entry) => ({
1615
+ ...entry,
1616
+ pinned: log.pins.has(entry.id),
1617
+ undone: undone.has(entry.id)
1618
+ }));
1619
+ }
1620
+ function peekOf(log, point) {
1621
+ const files = manifestAt(log, point);
1622
+ return files && Object.fromEntries(files);
1623
+ }
1624
+ function withoutHiddenPaths(log) {
1625
+ for (const path of log.baseline.keys()) if (!isHistoryPath(path)) log.baseline.delete(path);
1626
+ for (const entry of log.entries)
1627
+ entry.files = entry.files.filter((file) => isHistoryPath(file.path));
1628
+ return log;
1629
+ }
1630
+ var historyFiles = (dir) => listProjectFiles(dir).filter((file) => isHistoryPath(file.path));
1631
+ var COPY_LATER_ABOVE_BYTES = 1024 ** 2;
1632
+ var sameWho = (a, b) => a.kind === b.kind && a.name === b.name;
1633
+ function splitAt(change, at, kept) {
1634
+ if (at === change.after) return [change, null];
1635
+ if (!at || at === change.before || !kept) return [null, change];
1636
+ return [
1637
+ { ...change, after: at },
1638
+ { ...change, before: at }
1639
+ ];
1640
+ }
1641
+ var isMissingFolder = (error) => ["ENOENT", "ENOTDIR"].includes(error.code ?? "");
1642
+ async function removeEmptyFolders(dir) {
1643
+ let entries;
1644
+ try {
1645
+ entries = await readdir3(dir, { withFileTypes: true });
1646
+ } catch (error) {
1647
+ if (isMissingFolder(error)) return;
1648
+ throw error;
1649
+ }
1650
+ for (const entry of entries)
1651
+ if (entry.isDirectory()) await removeEmptyFolders(join8(dir, entry.name));
1652
+ await rmdir(dir);
1653
+ }
1654
+ function fileLeftIn(dir, at, deleted) {
1655
+ for (const entry of readdirSync4(dir, { withFileTypes: true })) {
1656
+ const path = `${at}/${entry.name}`;
1657
+ const left = entry.isDirectory() ? fileLeftIn(join8(dir, entry.name), path, deleted) : deleted.has(path) ? void 0 : path;
1658
+ if (left) return left;
1659
+ }
1660
+ return void 0;
1661
+ }
1662
+ function ignoresCase(dir) {
1663
+ const self = statSync5(join8(dir, ID_PATH), { throwIfNoEntry: false });
1664
+ const other = statSync5(join8(dir, ID_PATH.toUpperCase()), { throwIfNoEntry: false });
1665
+ return self !== void 0 && other?.ino === self.ino && other.dev === self.dev;
1666
+ }
1667
+ var blocks = (removed, added) => added.startsWith(`${removed}/`) || removed.startsWith(`${added}/`);
1668
+ var MAX_TIMER_MS = 2 ** 31 - 1;
1669
+ var takesWrite = (window, at) => window.idleMs === void 0 || at - (window.lastWriteAt ?? at) <= window.idleMs;
1670
+ function chainBackFrom(byAfter, hash) {
1671
+ const chain = /* @__PURE__ */ new Map();
1672
+ let at = hash;
1673
+ let prior = byAfter?.get(at);
1674
+ while (prior !== void 0 && !chain.has(at)) {
1675
+ chain.set(at, prior);
1676
+ at = prior;
1677
+ prior = byAfter?.get(at);
1678
+ }
1679
+ return chain;
1680
+ }
1681
+ function addChange(group, path, before, after) {
1682
+ const earlier = group.changes.get(path);
1683
+ const from = earlier ? earlier.before : before;
1684
+ if (from === after) group.changes.delete(path);
1685
+ else group.changes.set(path, { path, before: from, after });
1686
+ }
1687
+ var targetKey = (target) => JSON.stringify([target.file, target.id, target.hfId, target.selector, target.selectorIndex]);
1688
+ function keepTargetsOfItsFiles(entry) {
1689
+ const targets = entry.targets?.filter(
1690
+ (target) => entry.files.some((file) => file.path === target.file)
1691
+ );
1692
+ if (targets?.length) entry.targets = targets;
1693
+ else delete entry.targets;
1694
+ return entry;
1695
+ }
1696
+ var Engine = class {
1697
+ constructor(options, projectId, blobs) {
1698
+ this.options = options;
1699
+ this.projectId = projectId;
1700
+ this.blobs = blobs;
1701
+ this.dir = resolve7(options.projectDir);
1702
+ this.home = join8(options.historyRoot, projectId);
1703
+ this.foldsCase = ignoresCase(this.dir);
1704
+ const folder = statSync5(this.dir, { throwIfNoEntry: false });
1705
+ if (!folder || readId(this.dir) !== projectId || !isRecordedFolder(this.home, folder))
1706
+ throw this.replaced();
1707
+ this.folder = { dev: folder.dev, ino: folder.ino, birthtimeMs: folder.birthtimeMs };
1708
+ }
1709
+ options;
1710
+ projectId;
1711
+ blobs;
1712
+ dir;
1713
+ foldsCase;
1714
+ folder;
1715
+ home;
1716
+ log = { baseline: /* @__PURE__ */ new Map(), entries: [], pins: /* @__PURE__ */ new Set() };
1717
+ tracked = /* @__PURE__ */ new Map();
1718
+ windows = [];
1719
+ outside = null;
1720
+ /** A coalescing claim, open until another key, its idle timer, an operation, a window, or another write. */
1721
+ claimed = null;
1722
+ adopting = /* @__PURE__ */ new Map();
1723
+ /** Per path and hash an API write left, the hash of the bytes it replaced, until walked or written past. */
1724
+ overwritten = /* @__PURE__ */ new Map();
1725
+ stopHearing;
1726
+ writeToken;
1727
+ quietTimer;
1728
+ maxTimer;
1729
+ notedTimer = null;
1730
+ listeners = /* @__PURE__ */ new Set();
1731
+ tail = Promise.resolve();
1732
+ closed = false;
1733
+ closing;
1734
+ /** A path as the project's disk compares names: folded where `A` and `a` are one name. */
1735
+ nameKey(path) {
1736
+ return this.foldsCase ? path.toLowerCase() : path;
1737
+ }
1738
+ get logFile() {
1739
+ return logFileIn(this.home);
1740
+ }
1741
+ now() {
1742
+ return (this.options.now ?? Date.now)();
1743
+ }
1744
+ /** One operation at a time, in order: sweeps, windows and undos never interleave. */
1745
+ queue(task) {
1746
+ if (this.closed)
1747
+ return Promise.reject(new HistoryClosedError("This project's history is closed."));
1748
+ const run = this.tail.then(async () => {
1749
+ try {
1750
+ return await task();
1751
+ } finally {
1752
+ if (this.whereFolder() === "here") {
1753
+ await writeHistoryState(
1754
+ this.dir,
1755
+ this.projectId,
1756
+ this.home,
1757
+ this.log.entries,
1758
+ this.manifest(),
1759
+ manifestAt(this.log, this.log.entries.at(-1)?.id ?? START) ?? /* @__PURE__ */ new Map()
1760
+ ).catch((error) => this.options.onError?.(error));
1761
+ }
1762
+ }
1763
+ });
1764
+ this.tail = run.catch(() => void 0);
1765
+ return run;
1766
+ }
1767
+ /** Another project if the folder or its id changed, or the id is gone: a recreated folder may reuse the inode. */
1768
+ whereFolder() {
1769
+ const now = statSync5(this.dir, { throwIfNoEntry: false });
1770
+ if (!now) return "gone";
1771
+ const same = now.dev === this.folder.dev && sameFolder(now, this.folder);
1772
+ return same && readId(this.dir) === this.projectId ? "here" : "replaced";
1773
+ }
1774
+ assertWritable() {
1775
+ const folder = this.whereFolder();
1776
+ if (folder === "gone") throw new Error(`The project folder is gone: ${this.dir}`);
1777
+ if (folder === "replaced") throw this.replaced();
1778
+ }
1779
+ persistLog(record) {
1780
+ if (record) saveRecord(this.logFile, this.log, record);
1781
+ else writeLog(this.logFile, this.log);
1782
+ if (!existsSync3(join8(this.home, "project.json")))
1783
+ recordProject(this.home, this.dir, this.folder);
1784
+ }
1785
+ replaced() {
1786
+ return new HistoryClosedError(`${this.dir} is now another project.`);
1787
+ }
1788
+ assertOpen() {
1789
+ if (this.closed) throw new HistoryClosedError("This project's history is closed.");
1790
+ if (this.whereFolder() === "replaced") throw this.replaced();
1791
+ }
1792
+ async start() {
1793
+ const log = readLog(
1794
+ this.logFile,
1795
+ (line) => this.options.onError?.(
1796
+ new Error(`History log line ${line} could not be read and was skipped.`)
1797
+ )
1798
+ );
1799
+ if (!log) return this.firstOpen();
1800
+ this.log = withoutHiddenPaths(log);
1801
+ await this.reclaimUncopied();
1802
+ if (!this.log.keepsLedger) await this.takeInUnnamedLedger();
1803
+ const cache = this.readStatCache();
1804
+ const last = this.log.entries.at(-1)?.id ?? START;
1805
+ for (const [path, hash] of manifestAt(this.log, last) ?? []) {
1806
+ const cached = cache.get(path);
1807
+ this.tracked.set(path, { hash, stat: cached?.hash === hash ? cached.stat : "" });
1808
+ }
1809
+ const left = this.readAdopting();
1810
+ const unadopted = (file) => left.has(file.path) && !this.tracked.has(file.path);
1811
+ this.adoptMediaInBackground(historyFiles(this.dir).filter(unadopted), this.now());
1812
+ this.reopenClosedWindow();
1813
+ await this.settleAll();
1814
+ }
1815
+ /** Copies an older version took of files that are no longer copied; text and small files are never touched. */
1816
+ async reclaimUncopied() {
1817
+ await this.blobs.drop(uncopiedOnlyHashes(this.log, (hash) => this.blobs.size(hash)));
1818
+ }
1819
+ hearWrites() {
1820
+ const realDir = realFilePath(this.dir);
1821
+ this.stopHearing = onFileOverwritten((absPath, version, bytes) => {
1822
+ const after = hashOfVersion(version);
1823
+ if (!after || !affectsProjectSignature(realDir, absPath)) return;
1824
+ const replaced = hashOfVersion(fileContentVersion(bytes));
1825
+ if (!replaced || replaced === after) return;
1826
+ const path = relative5(realDir, absPath).split(sep5).join("/");
1827
+ if (!isHistoryPath(path)) return;
1828
+ this.overwritten.set(
1829
+ path,
1830
+ chainBackFrom(this.overwritten.get(path), replaced).set(after, replaced)
1831
+ );
1832
+ this.queue(async () => {
1833
+ if (!this.blobs.has(replaced) && !isUncopied(path, bytes.length))
1834
+ await this.storeBytes(bytes);
1835
+ }).catch((error) => this.options.onError?.(error));
1836
+ });
1837
+ }
1838
+ async storeBytes(bytes) {
1839
+ const staged = join8(this.home, `overwrote-${randomUUID7()}`);
1840
+ await mkdir2(this.home, { recursive: true });
1841
+ await writeFile(staged, bytes);
1842
+ try {
1843
+ await this.blobs.put(staged);
1844
+ } finally {
1845
+ await rm3(staged, { force: true });
1846
+ }
1847
+ }
1848
+ /** Read at a state no API write left, with no write heard since `heard`: no walk passes it, so forget the rest. */
1849
+ forgetWritesBefore(path, hash, heard) {
1850
+ if (this.overwritten.get(path) === heard && !(hash && heard?.has(hash)))
1851
+ this.overwritten.delete(path);
1852
+ }
1853
+ reopenClosedWindow() {
1854
+ const closed = this.options.closedWindow;
1855
+ if (!closed || this.log.entries.some((entry) => entry.id === closed.id)) return;
1856
+ const { id, who, label, startedAt, lastWriteAt, idleMs } = closed;
1857
+ this.windows.push({ id, who, label, startedAt, lastWriteAt, idleMs, changes: /* @__PURE__ */ new Map() });
1858
+ }
1859
+ async takeInUnnamedLedger() {
1860
+ const named = (path) => this.log.entries.some((entry) => entry.files.some((file) => file.path === path));
1861
+ const listed = () => historyFiles(this.dir).some((file) => file.path === MEDIA_LEDGER);
1862
+ const hash = this.log.baseline.has(MEDIA_LEDGER) || named(MEDIA_LEDGER) || !listed() ? null : await this.storeIfPresent(MEDIA_LEDGER);
1863
+ if (hash !== null) this.log.baseline.set(MEDIA_LEDGER, hash);
1864
+ this.log.keepsLedger = true;
1865
+ this.persistLog();
1866
+ }
1867
+ async firstOpen() {
1868
+ const sweptAt = this.now();
1869
+ const files = historyFiles(this.dir);
1870
+ for (const file of files.filter((file2) => file2.size <= COPY_LATER_ABOVE_BYTES)) {
1871
+ const hash = await this.storeIfPresent(file.path);
1872
+ if (this.whereFolder() !== "here") throw this.replaced();
1873
+ if (hash !== null) this.tracked.set(file.path, { hash, stat: statKey(file, sweptAt) });
1874
+ }
1875
+ this.log.baseline = this.manifest();
1876
+ this.log.keepsLedger = true;
1877
+ this.persistLog();
1878
+ this.saveStatCache();
1879
+ this.adoptMediaInBackground(
1880
+ files.filter((file) => file.size > COPY_LATER_ABOVE_BYTES),
1881
+ sweptAt
1882
+ );
1883
+ }
1884
+ adoptMediaInBackground(files, sweptAt) {
1885
+ let copied = Promise.resolve();
1886
+ for (const file of files) {
1887
+ copied = copied.then(() => this.adopt(file, sweptAt));
1888
+ this.adopting.set(file.path, copied);
1889
+ }
1890
+ this.saveAdopting();
1891
+ }
1892
+ saveAdopting() {
1893
+ const file = join8(this.home, "adopting.json");
1894
+ if (!this.adopting.size) return rmSync3(file, { force: true });
1895
+ mkdirSync5(this.home, { recursive: true });
1896
+ replaceFileAtomically(file, JSON.stringify([...this.adopting.keys()]), 420);
1897
+ }
1898
+ async adopt(file, sweptAt) {
1899
+ try {
1900
+ if (this.whereFolder() !== "here") return;
1901
+ const stored = await this.storeIfPresent(file.path);
1902
+ await this.queue(async () => {
1903
+ await this.recordAdopted(file, sweptAt, stored);
1904
+ this.adopting.delete(file.path);
1905
+ this.saveAdopting();
1906
+ });
1907
+ } catch (error) {
1908
+ if (!(error instanceof HistoryClosedError)) this.options.onError?.(error);
1909
+ }
1910
+ }
1911
+ async recordAdopted(file, sweptAt, stored) {
1912
+ if (stored === null || this.whereFolder() !== "here") return;
1913
+ const kept = this.blobs.has(stored) || isUncopied(file.path, file.size);
1914
+ const hash = kept ? stored : await this.storeIfPresent(file.path);
1915
+ if (hash === null) return;
1916
+ const change = { path: file.path, before: null, after: hash };
1917
+ const found = this.overwrittenBy(change, void 0, /* @__PURE__ */ new Set());
1918
+ const start = found && this.blobs.has(found) ? found : hash;
1919
+ this.tracked.set(file.path, { hash, stat: statKey(file, sweptAt) });
1920
+ this.log.baseline.set(file.path, start);
1921
+ if (start !== hash) addChange(this.outsideGroup(), file.path, start, hash);
1922
+ this.persistLog(baselineRecord(this.log));
1923
+ this.saveStatCache();
1924
+ }
1925
+ manifest() {
1926
+ return new Map([...this.tracked].map(([path, file]) => [path, file.hash]));
1927
+ }
1928
+ readStatCache() {
1929
+ try {
1930
+ const files = JSON.parse(readFileSync10(join8(this.home, "stat.json"), "utf-8"));
1931
+ return new Map(Object.entries(files));
1932
+ } catch {
1933
+ return /* @__PURE__ */ new Map();
1934
+ }
1935
+ }
1936
+ readAdopting() {
1937
+ try {
1938
+ return new Set(
1939
+ JSON.parse(readFileSync10(join8(this.home, "adopting.json"), "utf-8"))
1940
+ );
1941
+ } catch {
1942
+ return /* @__PURE__ */ new Set();
1943
+ }
1944
+ }
1945
+ saveStatCache() {
1946
+ const files = JSON.stringify(Object.fromEntries(this.tracked));
1947
+ mkdirSync5(this.home, { recursive: true });
1948
+ replaceFileAtomically(join8(this.home, "stat.json"), files, 420);
1949
+ }
1950
+ /**
1951
+ * Takes in every write since the last sweep and files each change to its writer. ponytail: always the whole
1952
+ * project (a stat per file, a hash only when the stat moved); per-path sweeps if projects reach tens of thousands.
1953
+ */
1954
+ async sweep() {
1955
+ if (this.whereFolder() !== "here") return;
1956
+ const sweptAt = this.now();
1957
+ const changedAt = (file) => Math.min(sweptAt, Math.max(file.mtimeMs, file.ctimeMs));
1958
+ const seen = historyFiles(this.dir).filter((file) => !this.adopting.has(file.path));
1959
+ const heard = new Map(this.overwritten);
1960
+ const present = new Set(seen.map((file) => file.path));
1961
+ const removed = [...this.tracked.keys()].filter((path) => !present.has(path)).map((path) => {
1962
+ const standing = this.standingAt(path);
1963
+ return { at: standing ? changedAt(standing.stat) : sweptAt, path, file: null };
1964
+ });
1965
+ const appearedAt = (file) => Math.max(
1966
+ changedAt(file),
1967
+ ...removed.filter((gone) => blocks(this.nameKey(gone.path), this.nameKey(file.path))).map((gone) => gone.at)
1968
+ );
1969
+ const events = [
1970
+ ...removed,
1971
+ ...seen.map((file) => ({ at: appearedAt(file), path: file.path, file }))
1972
+ ].sort((a, b) => a.at - b.at);
1973
+ let changed = false;
1974
+ for (const { at, path, file } of events) {
1975
+ if (file) {
1976
+ changed = await this.observe(path, statKey(file, sweptAt), at) || changed;
1977
+ continue;
1978
+ }
1979
+ const known = this.tracked.get(path);
1980
+ this.tracked.delete(path);
1981
+ this.forgetWritesBefore(path, null, heard.get(path));
1982
+ await this.record(path, known.hash, null, at);
1983
+ changed = true;
1984
+ }
1985
+ if (changed) this.saveStatCache();
1986
+ }
1987
+ /** The file's hash once copied in; null when it was removed before the copy (the next sweep records that). */
1988
+ async storeIfPresent(path) {
1989
+ try {
1990
+ const abs = join8(this.dir, path);
1991
+ const { size } = await stat2(abs);
1992
+ return await (isUncopied(path, size) ? this.blobs.reference(abs) : this.blobs.put(abs));
1993
+ } catch (error) {
1994
+ if (error.code === "ENOENT") return null;
1995
+ throw error;
1996
+ }
1997
+ }
1998
+ async observe(path, stat4, at) {
1999
+ const known = this.tracked.get(path) ?? { hash: null, stat: null };
2000
+ if (stat4 && known.stat === stat4) return false;
2001
+ const heard = this.overwritten.get(path);
2002
+ const hash = await this.storeIfPresent(path);
2003
+ if (hash === null || this.whereFolder() !== "here") return false;
2004
+ this.tracked.set(path, { hash, stat: stat4 });
2005
+ this.forgetWritesBefore(path, hash, heard);
2006
+ if (known.hash !== hash) await this.record(path, known.hash, hash, at);
2007
+ return known.hash !== hash || known.stat !== stat4;
2008
+ }
2009
+ async record(path, before, after, at) {
2010
+ const endedBeforeThisWrite = this.windows.filter((open) => !takesWrite(open, at));
2011
+ if (endedBeforeThisWrite.length) await this.commitOutside();
2012
+ for (const window2 of endedBeforeThisWrite) await this.endWindow(window2);
2013
+ const window = this.windows.at(-1);
2014
+ if (window) this.touch(window, at);
2015
+ addChange(window ?? this.outsideGroup(), path, before, after);
2016
+ }
2017
+ /** ponytail: another's write between Studio's write and its sweep folds into Studio's entry (per-write tokens). */
2018
+ async claimNow(who, label, paths, { coalesceKey, idleMs, overwrote = {}, targets = [] }) {
2019
+ await this.sweep();
2020
+ const taken = await this.takeClaimed(who, paths, overwrote);
2021
+ if (!taken.length) return this.claimNothing(coalesceKey);
2022
+ const held = this.heldFor(coalesceKey, taken);
2023
+ if (!held) await this.commitClaim();
2024
+ await this.commitPending();
2025
+ const group = held ?? this.newGroup(who, label);
2026
+ for (const change of taken) addChange(group, change.path, change.before, change.after);
2027
+ this.addTargets(group, targets);
2028
+ if (coalesceKey) return this.holdClaim(group, coalesceKey, idleMs);
2029
+ const entry = await this.commit(group);
2030
+ return entry && { id: entry.id };
2031
+ }
2032
+ addTargets(group, targets) {
2033
+ const unique = new Map((group.targets ?? []).map((target) => [targetKey(target), target]));
2034
+ for (const target of targets) {
2035
+ const normalized = { ...target, file: this.logPath(target.file) };
2036
+ if (group.changes.has(normalized.file)) unique.set(targetKey(normalized), normalized);
2037
+ }
2038
+ group.targets = [...unique.values()];
2039
+ }
2040
+ /** A claim that took nothing still ends a held claim under another key. */
2041
+ async claimNothing(coalesceKey) {
2042
+ if (coalesceKey !== this.claimed?.key) await this.commitClaim();
2043
+ return null;
2044
+ }
2045
+ /** Commits every other writer's pending changes, oldest first; open windows stay open. */
2046
+ async commitPending() {
2047
+ await this.commitOutside();
2048
+ for (const window of [...this.windows])
2049
+ if (window.changes.size) await this.commitCut(window, [...window.changes.keys()]);
2050
+ }
2051
+ pendingElsewhere() {
2052
+ return Boolean(this.outside?.changes.size) || this.windows.some((open) => open.changes.size > 0);
2053
+ }
2054
+ /** The held claim `taken` continues: same key, each file from its own last change, no held file changed since. */
2055
+ heldFor(key, taken) {
2056
+ const group = key && this.claimed?.key === key && !this.pendingElsewhere() ? this.claimed.group : null;
2057
+ const after = (path, before) => {
2058
+ const own = group?.changes.get(path);
2059
+ return own ? own.after : before;
2060
+ };
2061
+ const takenPaths = new Set(taken.map((change) => change.path));
2062
+ const untouched = (change) => takenPaths.has(change.path) || (this.tracked.get(change.path)?.hash ?? null) === change.after;
2063
+ return group && taken.every((change) => after(change.path, change.before) === change.before) && [...group.changes.values()].every(untouched) ? group : null;
2064
+ }
2065
+ logPath(path) {
2066
+ return relative5(this.dir, resolve7(this.dir, path)).split(sep5).join("/");
2067
+ }
2068
+ /** Takes `paths`' uncommitted changes from others, cut at what `who` overwrote. */
2069
+ async takeClaimed(who, paths, overwrote) {
2070
+ const wanted = new Set(paths.map((path) => this.logPath(path)));
2071
+ const at = new Map(
2072
+ Object.entries(overwrote).map(([path, version]) => [
2073
+ this.logPath(path),
2074
+ hashOfVersion(version)
2075
+ ])
2076
+ );
2077
+ const others = this.windows.filter((open) => !sameWho(open.who, who));
2078
+ const groups = this.outside ? [this.outside, ...others] : others;
2079
+ const hits = groups.flatMap(
2080
+ (group) => [...group.changes.values()].filter((change) => wanted.has(change.path)).map((change) => ({ group, change }))
2081
+ );
2082
+ const cuts = [];
2083
+ const walked = hits.map(() => /* @__PURE__ */ new Set());
2084
+ for (const [i, { change }] of hits.entries()) {
2085
+ const told = at.get(change.path);
2086
+ cuts.push(this.overwrittenBy(change, told, walked[i]) ?? told);
2087
+ }
2088
+ hits.forEach(({ change }, i) => this.forgetOverwritten(change.path, walked[i]));
2089
+ return hits.flatMap(({ group, change }, i) => this.cutOut(group, change, cuts[i]) ?? []);
2090
+ }
2091
+ /** Walks the server's writes back from `change.after` to the stored bytes they replaced. */
2092
+ overwrittenBy(change, told, seen) {
2093
+ let found;
2094
+ for (let hash = change.after; hash; ) {
2095
+ if (seen.has(hash)) return void 0;
2096
+ seen.add(hash);
2097
+ const replaced = this.overwritten.get(change.path)?.get(hash);
2098
+ if (replaced === void 0) break;
2099
+ found = hash = replaced;
2100
+ if (hash === change.before || hash === told) return hash;
2101
+ }
2102
+ return found;
2103
+ }
2104
+ forgetOverwritten(path, hashes) {
2105
+ const byAfter = this.overwritten.get(path);
2106
+ for (const hash of hashes) byAfter?.delete(hash);
2107
+ if (byAfter?.size === 0) this.overwritten.delete(path);
2108
+ }
2109
+ cutOut(group, change, cut) {
2110
+ const [kept, claimed] = splitAt(change, cut, cut !== void 0 && this.blobs.has(cut));
2111
+ group.changes.delete(change.path);
2112
+ if (kept) group.changes.set(kept.path, kept);
2113
+ return claimed;
2114
+ }
2115
+ /** Commits the window's cut `paths` as their own entry, other files staying: a window holds one change per file,
2116
+ * so the writer's next write to a cut file would otherwise span the claimer's edit. */
2117
+ async commitCut(window, paths) {
2118
+ const part = { ...this.newGroup(window.who, window.label), startedAt: window.startedAt };
2119
+ for (const path of paths) {
2120
+ part.changes.set(path, window.changes.get(path));
2121
+ window.changes.delete(path);
2122
+ }
2123
+ (window.parts ??= /* @__PURE__ */ new Set()).add(part.id);
2124
+ window.entry = await this.commit(part);
2125
+ }
2126
+ holdClaim(group, key, idleMs = this.options.quietMs ?? 2e3) {
2127
+ clearTimeout(this.claimed?.timer);
2128
+ const timer = Number.isFinite(idleMs) ? setTimeout(() => this.background(() => this.commitClaim()), idleMs) : void 0;
2129
+ timer?.unref?.();
2130
+ group.lastWriteAt = this.now();
2131
+ this.claimed = { group, key, timer };
2132
+ return group.changes.size ? { id: group.id } : null;
2133
+ }
2134
+ async commitClaim() {
2135
+ const held = this.claimed;
2136
+ this.claimed = null;
2137
+ if (!held) return;
2138
+ clearTimeout(held.timer);
2139
+ await this.commit(held.group);
2140
+ }
2141
+ outsideGroup() {
2142
+ const { quietMs = 2e3, maxGroupMs = 3e4 } = this.options;
2143
+ const closeIn = (ms) => {
2144
+ const timer = setTimeout(() => this.background(() => this.commitOutside()), ms);
2145
+ timer.unref?.();
2146
+ return timer;
2147
+ };
2148
+ if (!this.outside) {
2149
+ this.outside = this.newGroup(OUTSIDE, OUTSIDE_LABEL);
2150
+ this.maxTimer = closeIn(maxGroupMs);
2151
+ }
2152
+ clearTimeout(this.quietTimer);
2153
+ this.quietTimer = closeIn(quietMs);
2154
+ return this.outside;
2155
+ }
2156
+ newGroup(who, label) {
2157
+ return { id: randomUUID7(), who, label, startedAt: this.now(), changes: /* @__PURE__ */ new Map() };
2158
+ }
2159
+ async commitOutside() {
2160
+ if (this.outside?.changes.size) await this.commitClaim();
2161
+ clearTimeout(this.quietTimer);
2162
+ clearTimeout(this.maxTimer);
2163
+ const group = this.outside;
2164
+ this.outside = null;
2165
+ if (group) await this.commit(group);
2166
+ }
2167
+ async commit(group, extra = {}) {
2168
+ const undoesEmptied = this.log.entries.find((entry2) => entry2.id === extra.undoes)?.files.length === 0;
2169
+ if (!group.changes.size && !undoesEmptied) return null;
2170
+ const pending = this.pendingEntry(group);
2171
+ const endedAt = Math.max(pending.endedAt, this.log.entries.at(-1)?.endedAt ?? 0);
2172
+ const entry = keepTargetsOfItsFiles({ ...pending, endedAt, ...extra });
2173
+ this.log.entries.push(entry);
2174
+ try {
2175
+ this.persistLog({ type: "entry", entry });
2176
+ } catch (error) {
2177
+ this.log.entries.pop();
2178
+ throw error;
2179
+ }
2180
+ for (const listener of this.listeners) listener(entry);
2181
+ for (const change of group.changes.values()) {
2182
+ const walked = /* @__PURE__ */ new Set();
2183
+ this.overwrittenBy(change, void 0, walked);
2184
+ this.forgetOverwritten(change.path, walked);
2185
+ }
2186
+ await this.keepWithinBudget();
2187
+ return entry;
2188
+ }
2189
+ /** Stored bytes beyond what the current files need: a project larger than the budget still keeps its history. */
2190
+ historyBytes() {
2191
+ let current = 0;
2192
+ for (const hash of new Set([...this.tracked.values()].map((file) => file.hash)))
2193
+ current += this.blobs.size(hash);
2194
+ return this.blobs.bytes() - current;
2195
+ }
2196
+ async keepWithinBudget() {
2197
+ const budget = this.options.budgetBytes ?? 2 * 1024 ** 3;
2198
+ let folded = false;
2199
+ while (this.historyBytes() > budget) {
2200
+ await this.blobs.prune(
2201
+ /* @__PURE__ */ new Set([
2202
+ ...referencedHashes(this.log, this.manifest()),
2203
+ ...this.pendingHashes(),
2204
+ ...[...this.overwritten.values()].flatMap((byAfter) => [...byAfter.values()])
2205
+ ])
2206
+ );
2207
+ if (this.historyBytes() <= budget || !foldOldest(this.log)) break;
2208
+ folded = true;
2209
+ }
2210
+ if (folded) this.persistLog();
2211
+ }
2212
+ /** Hashes only pending changes point at yet, such as a claim's stored cut. */
2213
+ pendingHashes() {
2214
+ const changes = [...this.windows, this.outside, this.claimed?.group].flatMap((group) => [
2215
+ ...group?.changes.values() ?? []
2216
+ ]);
2217
+ return changes.flatMap((change) => [change.before, change.after]).filter((hash) => hash !== null);
2218
+ }
2219
+ /** Before an operation or a window: every write is filed and every pending change committed, in write order. */
2220
+ async settle() {
2221
+ await this.sweep();
2222
+ await this.commitClaim();
2223
+ await this.commitPending();
2224
+ }
2225
+ /** A watcher saw a write: one sweep per burst takes it in (a deleted folder is reported by its name alone). */
2226
+ noteChange(path) {
2227
+ const absolute = resolve7(this.dir, path);
2228
+ if (this.notedTimer || !affectsProjectSignature(this.dir, absolute)) return;
2229
+ if (!isHistoryPath(relative5(this.dir, absolute).split(sep5).join("/"))) return;
2230
+ this.notedTimer = setTimeout(() => {
2231
+ this.notedTimer = null;
2232
+ this.background(() => this.sweep());
2233
+ }, 20);
2234
+ this.notedTimer.unref?.();
2235
+ }
2236
+ background(task) {
2237
+ this.queue(task).catch((error) => {
2238
+ if (!(error instanceof HistoryClosedError)) this.options.onError?.(error);
2239
+ });
2240
+ }
2241
+ beginWindow(who, label, idleMs, exclusive = false) {
2242
+ return this.queue(async () => {
2243
+ await this.settle();
2244
+ const at = this.now();
2245
+ if (exclusive) {
2246
+ const blocking = this.windows.find(
2247
+ (open) => !sameWho(open.who, who) && takesWrite(open, at)
2248
+ );
2249
+ if (blocking)
2250
+ throw new Error(`${blocking.who.name}'s turn is open; it has to end before yours begins`);
2251
+ for (const open of [...this.windows])
2252
+ if (!takesWrite(open, at) || sameWho(open.who, who)) await this.endWindow(open);
2253
+ }
2254
+ const window = { ...this.newGroup(who, label), idleMs };
2255
+ this.windows.push(window);
2256
+ this.touch(window, this.now());
2257
+ const close = () => this.queue(() => this.sweepAndEnd(window));
2258
+ return { id: window.id, startedAt: window.startedAt, close };
2259
+ });
2260
+ }
2261
+ /** A window with no write for its idleMs ends, so a close that never comes cannot hold every later write. */
2262
+ touch(window, at) {
2263
+ window.lastWriteAt = Math.max(window.lastWriteAt ?? at, at);
2264
+ this.endWhenIdle(window, window.idleMs);
2265
+ }
2266
+ endWhenIdle(window, delay) {
2267
+ clearTimeout(window.idleTimer);
2268
+ if (window.idleMs === void 0 || !Number.isFinite(window.idleMs) || delay === void 0)
2269
+ return;
2270
+ window.idleTimer = setTimeout(
2271
+ () => {
2272
+ const left = window.idleMs - (this.now() - (window.lastWriteAt ?? this.now()));
2273
+ if (left > 0) this.endWhenIdle(window, left);
2274
+ else this.background(() => this.sweepAndEnd(window));
2275
+ },
2276
+ Math.min(delay, MAX_TIMER_MS)
2277
+ );
2278
+ window.idleTimer.unref?.();
2279
+ }
2280
+ async sweepAndEnd(window) {
2281
+ await this.sweep();
2282
+ return this.endWindow(window);
2283
+ }
2284
+ /** Commits an open window once; ending it again returns the entry it became. */
2285
+ async endWindow(window) {
2286
+ if (!this.windows.includes(window)) return window.entry ?? null;
2287
+ clearTimeout(window.idleTimer);
2288
+ await this.commitClaim();
2289
+ this.windows = this.windows.filter((open) => open !== window);
2290
+ window.entry = await this.commit(window) ?? window.entry ?? null;
2291
+ return window.entry;
2292
+ }
2293
+ /** Every pending write, open window and outside group, committed: for flush and close. */
2294
+ async settleAll() {
2295
+ await this.sweep();
2296
+ await this.commitClaim();
2297
+ for (const window of [...this.windows]) await this.endWindow(window);
2298
+ await this.commitOutside();
2299
+ }
2300
+ /** What took a removed path's place, dating its removal: an entry at the path, or a file at a folder above it. */
2301
+ standingAt(path) {
2302
+ const parts = path.split("/");
2303
+ for (let depth = 1; depth <= parts.length; depth += 1) {
2304
+ const at = parts.slice(0, depth).join("/");
2305
+ const stat4 = lstatSync4(join8(this.dir, at), { throwIfNoEntry: false });
2306
+ if (!stat4) return void 0;
2307
+ if (at === path || !stat4.isDirectory()) return { at, stat: stat4 };
2308
+ }
2309
+ return void 0;
2310
+ }
2311
+ /** 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. */
2312
+ inTheWay(path, deleted) {
2313
+ const standing = this.standingAt(path);
2314
+ if (!standing) return void 0;
2315
+ if (standing.stat.isSymbolicLink()) return standing.at;
2316
+ if (standing.at === path && !standing.stat.isDirectory()) return void 0;
2317
+ const real = realpathSync.native(join8(this.dir, standing.at));
2318
+ const at = relative5(realpathSync.native(this.dir), real).split(sep5).join("/");
2319
+ if (standing.at !== path) return deleted.has(at) ? void 0 : at;
2320
+ return fileLeftIn(real, at, deleted);
2321
+ }
2322
+ /** Writes `target` (path to hash, null deletes) as one entry of `who`'s. */
2323
+ async writeAs(who, label, target, extra) {
2324
+ this.assertWritable();
2325
+ const changes = [...target].filter(([path, hash]) => (this.tracked.get(path)?.hash ?? null) !== hash).sort(([, a], [, b]) => Number(a !== null) - Number(b !== null));
2326
+ const setAsideByDeletesFirst = new Set(
2327
+ changes.filter(([, hash]) => hash === null).map(([path]) => this.tracked.get(path)?.hash)
2328
+ );
2329
+ const gone = changes.find(
2330
+ ([, hash]) => hash !== null && !this.blobs.has(hash) && !setAsideByDeletesFirst.has(hash)
2331
+ );
2332
+ if (gone)
2333
+ throw new Error(
2334
+ `An earlier version of ${gone[0]} is not kept, so this cannot be brought back.`
2335
+ );
2336
+ const deleted = new Set(changes.filter(([, hash]) => hash === null).map(([path]) => path));
2337
+ const blocked = changes.filter(([, hash]) => hash !== null).map(([path]) => this.inTheWay(path, deleted)).find(Boolean);
2338
+ if (blocked) throw new Error(`${blocked} is in the way; move or delete it, then try again.`);
2339
+ const group = this.newGroup(who, label);
2340
+ this.windows.push(group);
2341
+ try {
2342
+ for (const [path, hash] of changes) await this.writeProjectFile(path, hash);
2343
+ await this.blobs.release(
2344
+ changes.flatMap(([path, hash]) => hash ? [[hash, path]] : [])
2345
+ );
2346
+ await this.sweep();
2347
+ } finally {
2348
+ this.windows = this.windows.filter((open) => open !== group);
2349
+ }
2350
+ return this.commit(group, extra);
2351
+ }
2352
+ /** Writes one project file (null deletes it), first leaving the running operation's receipt for its echo. */
2353
+ async writeProjectFile(path, hash) {
2354
+ this.assertWritable();
2355
+ const absPath = join8(this.dir, path);
2356
+ const { writeToken } = this;
2357
+ if (writeToken) {
2358
+ const version = hash === null ? DELETED_VERSION : hashVersion(hash);
2359
+ recordFileWriteReceipt(absPath, { path, version, writeToken });
2360
+ }
2361
+ const standing = lstatSync4(absPath, { throwIfNoEntry: false });
2362
+ if (standing?.isFile() && isUncopied(path, standing.size))
2363
+ await this.blobs.keep(path, absPath, hash === null);
2364
+ if (hash === null) {
2365
+ this.assertWritable();
2366
+ rmSync3(absPath, { force: true });
2367
+ } else {
2368
+ await removeEmptyFolders(absPath);
2369
+ await this.blobs.writeTo(hash, absPath, () => this.assertWritable());
2370
+ }
2371
+ }
2372
+ entry(id) {
2373
+ const entry = this.log.entries.find((candidate) => candidate.id === id);
2374
+ if (!entry) throw new Error("That change is no longer kept in this project's history.");
2375
+ return entry;
2376
+ }
2377
+ undoLabel(entry) {
2378
+ if (!entry.undoes) return `Undid: ${entry.label}`;
2379
+ const original = this.log.entries.find((candidate) => candidate.id === entry.undoes);
2380
+ return `Redid: ${original?.label ?? entry.label}`;
2381
+ }
2382
+ async undoNow(id, who, mode) {
2383
+ const entry = this.entry(id);
2384
+ const changed = this.movedOn(entry);
2385
+ if (changed.length && !mode) return { ok: false, conflict: this.conflict(entry, changed) };
2386
+ if (mode === "back-to-before") {
2387
+ const index = this.log.entries.indexOf(entry);
2388
+ const point = index > 0 ? this.log.entries[index - 1].id : START;
2389
+ return {
2390
+ ok: true,
2391
+ entry: await this.restoreNow(point, who, `Went back to before: ${entry.label}`)
2392
+ };
2393
+ }
2394
+ const target = new Map(
2395
+ entry.files.filter((file) => mode !== "keep-later-edits" || !changed.includes(file)).map((file) => [file.path, file.before])
2396
+ );
2397
+ return {
2398
+ ok: true,
2399
+ entry: await this.writeAs(who, this.undoLabel(entry), target, {
2400
+ undoes: id,
2401
+ ...entry.targets && { targets: entry.targets }
2402
+ })
2403
+ };
2404
+ }
2405
+ movedOn(entry) {
2406
+ return entry.files.filter((file) => (this.tracked.get(file.path)?.hash ?? null) !== file.after);
2407
+ }
2408
+ next(direction, who) {
2409
+ const mine = (author) => sameWho(author, who) || sameWho(author, OUTSIDE);
2410
+ const pending = [...[...this.windows].reverse(), this.outside, this.claimed?.group].find(
2411
+ (group) => group?.changes.size && mine(group.who)
2412
+ );
2413
+ if (pending) return direction === "back" ? this.pendingEntry(pending) : void 0;
2414
+ const everyone = this.options.undoScope === "everyone";
2415
+ const ofOpenTurn = (entry) => this.windows.some((open) => open.parts?.has(entry.id));
2416
+ return stepTarget(
2417
+ this.log.entries,
2418
+ direction,
2419
+ (entry) => entry.files.length > 0 && (mine(entry.who) || everyone && !ofOpenTurn(entry))
2420
+ );
2421
+ }
2422
+ /** A pending group as the entry it becomes once committed (a window's part gets a fresh id). */
2423
+ pendingEntry(group) {
2424
+ const files = [...group.changes.values()].sort((a, b) => a.path.localeCompare(b.path));
2425
+ const { id, who, label, startedAt, lastWriteAt } = group;
2426
+ return {
2427
+ id,
2428
+ who,
2429
+ label,
2430
+ startedAt,
2431
+ endedAt: lastWriteAt ?? this.now(),
2432
+ files,
2433
+ ...group.targets?.length && { targets: group.targets }
2434
+ };
2435
+ }
2436
+ conflict(entry, changed) {
2437
+ const paths = new Set(changed.map((file) => file.path));
2438
+ const after = this.log.entries.slice(this.log.entries.indexOf(entry) + 1);
2439
+ const newer = after.filter((later) => later.files.some((file) => paths.has(file.path)));
2440
+ return { files: [...paths], newer: newer.map((later) => later.id) };
2441
+ }
2442
+ async restoreNow(point, who, label) {
2443
+ const files = manifestAt(this.log, point);
2444
+ if (!files) throw new Error("That point is no longer kept in this project's history.");
2445
+ const target = new Map(
2446
+ [...this.tracked.keys()].map((path) => [path, null])
2447
+ );
2448
+ for (const [path, hash] of files) target.set(path, hash);
2449
+ if (point === START && this.adopting.size > 0) {
2450
+ throw new HistoryRefusedError(
2451
+ "The project's starting copy is still being saved; try again in a moment. Nothing was changed."
2452
+ );
2453
+ }
2454
+ const isProjectFile = (path) => !KEPT_HIDDEN_PATHS.has(path);
2455
+ const keepsAny = [...target].some(([path, hash]) => hash !== null && isProjectFile(path));
2456
+ if ([...this.tracked.keys()].some(isProjectFile) && !keepsAny) {
2457
+ throw new HistoryRefusedError(
2458
+ "Restoring this point would remove every file in this project, so nothing was changed."
2459
+ );
2460
+ }
2461
+ return this.writeAs(who, label, target, { restoredTo: point });
2462
+ }
2463
+ /** Settles, then runs `task` with its writes labelled `writeToken`. */
2464
+ operation(writeToken, task) {
2465
+ return this.queue(async () => {
2466
+ await this.settle();
2467
+ this.writeToken = writeToken;
2468
+ try {
2469
+ return await task();
2470
+ } finally {
2471
+ this.writeToken = void 0;
2472
+ }
2473
+ });
2474
+ }
2475
+ pointLabel(point) {
2476
+ return point === START ? "the start" : this.entry(point).label;
2477
+ }
2478
+ api() {
2479
+ return {
2480
+ projectId: this.projectId,
2481
+ beginWindow: (who, label, options = {}) => this.beginWindow(
2482
+ who,
2483
+ label,
2484
+ options.idleMs ?? this.options.maxGroupMs ?? 3e4,
2485
+ options.exclusive === true
2486
+ ),
2487
+ claim: async (who, label, paths, options = {}) => {
2488
+ await Promise.all(paths.map((path) => this.adopting.get(this.logPath(path))));
2489
+ return this.queue(() => this.claimNow(who, label, paths, options));
2490
+ },
2491
+ noteChange: (path) => {
2492
+ if (!this.closed) this.noteChange(path);
2493
+ },
2494
+ list: () => {
2495
+ this.assertOpen();
2496
+ return listOf(this.log);
2497
+ },
2498
+ step: (direction, who, { writeToken } = {}) => this.operation(writeToken, async () => {
2499
+ const target = this.next(direction, who);
2500
+ return target ? this.undoNow(target.id, who) : { ok: true, entry: null };
2501
+ }),
2502
+ undo: (id, { who, mode, writeToken }) => this.operation(writeToken, () => this.undoNow(id, who, mode)),
2503
+ restore: (point, who, { writeToken } = {}) => this.operation(
2504
+ writeToken,
2505
+ () => this.restoreNow(point, who, `Restored: ${this.pointLabel(point)}`)
2506
+ ),
2507
+ peek: (point) => {
2508
+ this.assertOpen();
2509
+ return peekOf(this.log, point);
2510
+ },
2511
+ checkout: (entryId, side, emptyDir) => this.queue(async () => {
2512
+ this.entry(entryId);
2513
+ if (!existsSync3(this.dir)) throw new Error(`The project folder is gone: ${this.dir}`);
2514
+ const dest = resolve7(emptyDir);
2515
+ if (isWithin(this.dir, dest))
2516
+ throw new Error(`Checkout writes outside the project only: ${dest}`);
2517
+ if ((await readdir3(dest).catch(missingIsEmpty)).length > 0)
2518
+ throw new Error(`Checkout writes into an empty folder only: ${dest}`);
2519
+ const files = manifestAround(this.log, entryId, side);
2520
+ for (const [path, hash] of files)
2521
+ if (!this.blobs.has(hash))
2522
+ throw new Error(`The version of ${path} from then is not kept.`);
2523
+ try {
2524
+ for (const [path, hash] of files) await this.blobs.writeTo(hash, join8(dest, path));
2525
+ } catch (error) {
2526
+ for (const name of await readdir3(dest).catch(() => []))
2527
+ await rm3(join8(dest, name), { recursive: true, force: true });
2528
+ throw error;
2529
+ }
2530
+ }),
2531
+ next: (direction, who) => {
2532
+ this.assertOpen();
2533
+ return this.next(direction, who);
2534
+ },
2535
+ readBlob: async (hash) => {
2536
+ this.assertOpen();
2537
+ return this.blobs.read(hash);
2538
+ },
2539
+ pin: (id, pinned) => {
2540
+ this.assertOpen();
2541
+ this.entry(id);
2542
+ if (pinned) this.log.pins.add(id);
2543
+ else this.log.pins.delete(id);
2544
+ this.persistLog({ type: "pin", id, pinned });
2545
+ },
2546
+ onEntry: (listener) => {
2547
+ this.listeners.add(listener);
2548
+ return () => this.listeners.delete(listener);
2549
+ },
2550
+ checkpoint: () => this.queue(() => this.settle()),
2551
+ flush: () => this.queue(() => this.settleAll()),
2552
+ replacedAtPath: () => this.whereFolder() === "replaced",
2553
+ close: () => this.closing ??= (async () => {
2554
+ await Promise.all(this.adopting.values());
2555
+ if (this.notedTimer) clearTimeout(this.notedTimer);
2556
+ this.stopHearing?.();
2557
+ const settled = this.queue(() => this.settleAll());
2558
+ this.closed = true;
2559
+ await settled;
2560
+ })()
2561
+ };
2562
+ }
2563
+ };
2564
+ function isWithin(dir, path) {
2565
+ let probe = resolve7(path);
2566
+ while (!existsSync3(probe) && dirname5(probe) !== probe) probe = dirname5(probe);
2567
+ const rel = relative5(realpathSync.native(dir), realpathSync.native(probe));
2568
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep5}`) && !isAbsolute5(rel);
2569
+ }
2570
+ function missingIsEmpty(error) {
2571
+ if (error.code === "ENOENT") return [];
2572
+ throw error;
2573
+ }
2574
+ var HistoryClosedError = class extends Error {
2575
+ constructor(message) {
2576
+ super(message);
2577
+ this.name = "HistoryClosedError";
2578
+ }
2579
+ };
2580
+ var HistoryRefusedError = class extends Error {
2581
+ constructor(message) {
2582
+ super(message);
2583
+ this.name = "HistoryRefusedError";
2584
+ }
2585
+ };
2586
+ async function openProjectHistory(options) {
2587
+ if (!existsSync3(options.projectDir))
2588
+ throw new HistoryClosedError(`${options.projectDir} is gone or now another project.`);
2589
+ const projectId = projectHistoryId(options.projectDir, options.historyRoot);
2590
+ const release = await takeHistoryOwnership(
2591
+ join8(options.historyRoot, projectId),
2592
+ options.ownerWaitMs ?? 5e3,
2593
+ options.ownerUrl
2594
+ );
2595
+ try {
2596
+ const blobs = await openBlobStore(join8(options.historyRoot, projectId, "blobs"));
2597
+ const closedWindow = options.closedWindowAfterOwnership?.() ?? options.closedWindow;
2598
+ const engine = new Engine({ ...options, closedWindow }, projectId, blobs);
2599
+ await engine.queue(async () => {
2600
+ await engine.start();
2601
+ engine.hearWrites();
2602
+ });
2603
+ pruneGoneProjectHistoriesDaily(options.historyRoot, engine.now(), {
2604
+ onError: options.onError,
2605
+ budgetMs: options.pruneGoneProjectsBudgetMs
2606
+ });
2607
+ const api = engine.api();
2608
+ return {
2609
+ ...api,
2610
+ close: () => api.close().finally(release)
2611
+ };
2612
+ } catch (error) {
2613
+ release();
2614
+ throw error;
2615
+ }
2616
+ }
2617
+ function readProjectHistory(options) {
2618
+ const dir = resolve7(options.projectDir);
2619
+ const id = readId(dir);
2620
+ const home = id && isRecordedFolder(join8(options.historyRoot, id), statSync5(dir)) ? join8(options.historyRoot, id) : null;
2621
+ const read = home && readLog(logFileIn(home), () => {
2622
+ });
2623
+ const log = withoutHiddenPaths(read || { baseline: /* @__PURE__ */ new Map(), entries: [], pins: /* @__PURE__ */ new Set() });
2624
+ return {
2625
+ list: () => listOf(log),
2626
+ peek: (point) => peekOf(log, point),
2627
+ readBlob: async (hash) => {
2628
+ if (!home) throw new Error("That is not a history blob.");
2629
+ return readBlob(join8(home, "blobs"), hash);
2630
+ }
2631
+ };
2632
+ }
2633
+ function historyIds(historyRoot) {
2634
+ try {
2635
+ return readdirSync5(historyRoot).filter(isHistoryId);
2636
+ } catch {
2637
+ return [];
2638
+ }
2639
+ }
2640
+ function namedHashes(log) {
2641
+ const hashes = new Set(log.baseline.values());
2642
+ for (const entry of log.entries) {
2643
+ for (const file of entry.files) {
2644
+ if (file.before) hashes.add(file.before);
2645
+ if (file.after) hashes.add(file.after);
2646
+ }
2647
+ }
2648
+ return hashes;
2649
+ }
2650
+ async function blobSizes(blobs, hashes) {
2651
+ const sizes = /* @__PURE__ */ new Map();
2652
+ for (const hash of hashes)
2653
+ sizes.set(hash, (await stat3(blobPath(blobs, hash)).catch(() => null))?.size ?? 0);
2654
+ return sizes;
2655
+ }
2656
+ async function reclaimOne(home, dryRun) {
2657
+ const log = readLog(join9(home, "log.jsonl"), () => {
2658
+ });
2659
+ if (!log) return 0;
2660
+ const blobs = join9(home, "blobs");
2661
+ const sizes = await blobSizes(blobs, namedHashes(log));
2662
+ let bytes = 0;
2663
+ for (const hash of uncopiedOnlyHashes(log, (known) => sizes.get(known) ?? 0)) {
2664
+ const size = sizes.get(hash) ?? 0;
2665
+ if (size === 0) continue;
2666
+ bytes += size;
2667
+ if (!dryRun) await rm4(blobPath(blobs, hash), { force: true });
2668
+ }
2669
+ return bytes;
2670
+ }
2671
+ async function reclaimUncopiedBlobs(historyRoot, { dryRun = false, onError } = {}) {
2672
+ const reclaimed = [];
2673
+ for (const id of historyIds(historyRoot)) {
2674
+ const home = join9(historyRoot, id);
2675
+ let release;
2676
+ try {
2677
+ release = await takeHistoryOwnership(home, 0);
2678
+ const bytes = await reclaimOne(home, dryRun);
2679
+ if (bytes > 0) reclaimed.push({ id, bytes });
2680
+ } catch (error) {
2681
+ if (!(error instanceof HistoryBusyError)) onError?.(error);
2682
+ } finally {
2683
+ release?.();
2684
+ }
2685
+ }
2686
+ return reclaimed;
2687
+ }
2688
+ function historyCache(open) {
2689
+ const histories = /* @__PURE__ */ new Map();
2690
+ const get = async (projectDir) => {
2691
+ let cached = histories.get(projectDir);
2692
+ if (!cached) histories.set(projectDir, cached = open(projectDir));
2693
+ const opened = await cached;
2694
+ if (!opened?.replacedAtPath()) return opened;
2695
+ const reopen = () => open(projectDir);
2696
+ if (histories.get(projectDir) === cached)
2697
+ histories.set(projectDir, opened.close().then(reopen, reopen));
2698
+ return get(projectDir);
2699
+ };
2700
+ return {
2701
+ get,
2702
+ /** The history if one was opened, without opening it. */
2703
+ peek: (projectDir) => histories.get(projectDir),
2704
+ forget: (projectDir) => histories.delete(projectDir),
2705
+ closeAll: () => Promise.all(
2706
+ [...histories.values()].map((opened) => opened.then((history) => history?.close()))
2707
+ )
2708
+ };
2709
+ }
2710
+
2711
+ export {
2712
+ fileContentVersion,
2713
+ DELETED_VERSION,
2714
+ settledFileTag,
2715
+ createWriteToken,
2716
+ recordFileWriteReceipt,
2717
+ clearFileWriteReceipt,
2718
+ identifyFileWrite,
2719
+ consumeFileWriteReceipt,
2720
+ HistoryBusyError,
2721
+ registerRenderOutput,
2722
+ STUDIO_SIGNATURE_MANIFEST_PATHS,
2723
+ affectsProjectSignature,
2724
+ resolveProjectSignature,
2725
+ resolveProjectAndSignature,
2726
+ createProjectSignature,
2727
+ HistoryIdError,
2728
+ listProjectHistories,
2729
+ pruneGoneProjectHistories,
2730
+ retainSnapshotOutput,
2731
+ beginArtifactCapture,
2732
+ recordArtifact,
2733
+ recordArtifacts,
2734
+ forgetRecordedArtifacts,
2735
+ artifactDirectory,
2736
+ recordRenderSuccess,
2737
+ finishSnapshotRun,
2738
+ START,
2739
+ DEFAULT_HISTORY_ROOT,
2740
+ MAX_WINDOW_IDLE_MS,
2741
+ UNDO_MODES,
2742
+ HistoryClosedError,
2743
+ HistoryRefusedError,
2744
+ openProjectHistory,
2745
+ readProjectHistory,
2746
+ reclaimUncopiedBlobs,
2747
+ historyCache
2748
+ };