@vfrey/vfrey 0.0.1 → 1.0.0-beta.1

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 (610) 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 +361 -0
  40. package/src/api/cloud-files.ts +197 -0
  41. package/src/api/cloud-runs.ts +75 -0
  42. package/src/api/cloud.ts +413 -0
  43. package/src/api/commands.ts +330 -0
  44. package/src/api/config.ts +34 -0
  45. package/src/api/creators.ts +463 -0
  46. package/src/api/credentials.ts +152 -0
  47. package/src/api/mail.ts +333 -0
  48. package/src/api/topics.ts +207 -0
  49. package/src/api/types.ts +273 -0
  50. package/src/cli.ts +468 -0
  51. package/src/cloud/bundle.ts +70 -0
  52. package/src/cloud/cloudrun-executor.ts +237 -0
  53. package/src/cloud/executor.ts +281 -0
  54. package/src/cloud/gcs-sign.ts +76 -0
  55. package/src/cloud/job.ts +106 -0
  56. package/src/cloud/remote-job.ts +67 -0
  57. package/src/cloud/render-job.ts +191 -0
  58. package/src/config.ts +309 -0
  59. package/src/dashboard-link.ts +180 -0
  60. package/src/dashboard-start.ts +103 -0
  61. package/src/desk/channel.ts +64 -0
  62. package/src/desk/cli.ts +216 -0
  63. package/src/desk/creators-route.ts +129 -0
  64. package/src/desk/fonts/JetBrainsMono-OFL.txt +93 -0
  65. package/src/desk/fonts/google-sans-flex-latin.woff2 +0 -0
  66. package/src/desk/fonts/jetbrains-mono-medium.woff2 +0 -0
  67. package/src/desk/http.ts +460 -0
  68. package/src/desk/mail-route.ts +103 -0
  69. package/src/desk/overlay.ts +112 -0
  70. package/src/desk/preview.ts +422 -0
  71. package/src/desk/screens.ts +149 -0
  72. package/src/desk/secrets.ts +143 -0
  73. package/src/desk/series.ts +127 -0
  74. package/src/desk/state.ts +77 -0
  75. package/src/desk/studio-route.ts +119 -0
  76. package/src/desk/tick-runner.ts +66 -0
  77. package/src/desk/tick.ts +56 -0
  78. package/src/desk/topics-route.ts +41 -0
  79. package/src/desk/ui/app.css +505 -0
  80. package/src/desk/ui/app.js +185 -0
  81. package/src/desk/ui/apple-icon.png +0 -0
  82. package/src/desk/ui/components.js +79 -0
  83. package/src/desk/ui/core.js +156 -0
  84. package/src/desk/ui/favicon.ico +0 -0
  85. package/src/desk/ui/icon.svg +1 -0
  86. package/src/desk/ui/icons.js +49 -0
  87. package/src/desk/ui/index.html +41 -0
  88. package/src/desk/ui/metrics-math.d.ts +22 -0
  89. package/src/desk/ui/metrics-math.js +135 -0
  90. package/src/desk/ui/theme.js +5 -0
  91. package/src/desk/ui/views-creators.js +209 -0
  92. package/src/desk/ui/views-keys.js +114 -0
  93. package/src/desk/ui/views-mail.js +188 -0
  94. package/src/desk/ui/views-misc.js +123 -0
  95. package/src/desk/ui/views-review.js +119 -0
  96. package/src/desk/ui/views-runs.js +350 -0
  97. package/src/desk/ui/views-screens.js +163 -0
  98. package/src/desk/ui/views-series.js +318 -0
  99. package/src/desk/ui/views-topics.js +87 -0
  100. package/src/desk/verbs.ts +361 -0
  101. package/src/doctor.ts +212 -0
  102. package/src/drain.ts +14 -0
  103. package/src/engage/index.ts +217 -0
  104. package/src/generate/index.ts +110 -0
  105. package/src/generate/script.ts +152 -0
  106. package/src/generate/title.ts +78 -0
  107. package/src/help.ts +252 -0
  108. package/src/home.ts +45 -0
  109. package/src/llm/client.ts +177 -0
  110. package/src/log.ts +7 -0
  111. package/src/oauth/tokens.ts +189 -0
  112. package/src/oauth/vault.ts +72 -0
  113. package/src/pipeline/review.ts +642 -0
  114. package/src/pipeline/run.ts +443 -0
  115. package/src/pipeline/series-guard.ts +26 -0
  116. package/src/pipeline/source-fetch.ts +69 -0
  117. package/src/pipeline/stages.ts +728 -0
  118. package/src/pipeline/topics-source.ts +90 -0
  119. package/src/product/commands.ts +296 -0
  120. package/src/product/config-file.ts +38 -0
  121. package/src/product/engine-choice.ts +89 -0
  122. package/src/product/engines.ts +103 -0
  123. package/src/product/insights.ts +50 -0
  124. package/src/product/keys.ts +67 -0
  125. package/src/product/paths.ts +18 -0
  126. package/src/product/probes.ts +88 -0
  127. package/src/product/push-flow.ts +90 -0
  128. package/src/product/render-video.ts +21 -0
  129. package/src/product/short.ts +62 -0
  130. package/src/product/update.ts +32 -0
  131. package/src/project/canonical.ts +14 -0
  132. package/src/project/commands.ts +177 -0
  133. package/src/project/dashboard-status.ts +28 -0
  134. package/src/project/discover.ts +19 -0
  135. package/src/project/index-file.ts +73 -0
  136. package/src/project/marker.ts +92 -0
  137. package/src/project/migrate.ts +201 -0
  138. package/src/project/names.ts +27 -0
  139. package/src/project/notice.ts +16 -0
  140. package/src/project/open.ts +88 -0
  141. package/src/project/scaffold.ts +112 -0
  142. package/src/project/templates.ts +42 -0
  143. package/src/project/videos.ts +64 -0
  144. package/src/publish/facebook.ts +91 -0
  145. package/src/publish/index.ts +12 -0
  146. package/src/publish/instagram.ts +97 -0
  147. package/src/publish/rate.ts +62 -0
  148. package/src/publish/session.ts +242 -0
  149. package/src/publish/tiktok.ts +202 -0
  150. package/src/publish/youtube.ts +177 -0
  151. package/src/queue/desk-ops.ts +1170 -0
  152. package/src/queue/seal.ts +3 -0
  153. package/src/queue/store.ts +1006 -0
  154. package/src/remotion/AiLabel.tsx +26 -0
  155. package/src/remotion/Root.tsx +73 -0
  156. package/src/remotion/defaults.ts +26 -0
  157. package/src/remotion/films/claude/ClaudeFilm.tsx +1014 -0
  158. package/src/remotion/films/engine/EngineMark.tsx +17 -0
  159. package/src/remotion/films/rea/ReaShort.tsx +552 -0
  160. package/src/remotion/films/rea/drawings.tsx +907 -0
  161. package/src/remotion/films/rea/timing.ts +144 -0
  162. package/src/remotion/index.ts +4 -0
  163. package/src/remotion/templates/EditorialSplit.tsx +39 -0
  164. package/src/remotion/templates/KineticType.tsx +39 -0
  165. package/src/remotion/templates/SignalBoard.tsx +50 -0
  166. package/src/remotion/timing.ts +22 -0
  167. package/src/render/chrome.ts +78 -0
  168. package/src/render/engine-mark.ts +29 -0
  169. package/src/render/framey-template.ts +130 -0
  170. package/src/render/html-engine.ts +149 -0
  171. package/src/render/node-crypto-stub.ts +11 -0
  172. package/src/render/probe.ts +29 -0
  173. package/src/render/props.ts +17 -0
  174. package/src/render/vertical-short.ts +35 -0
  175. package/src/render/webpack.ts +33 -0
  176. package/src/render/worker.ts +65 -0
  177. package/src/scene/assets/fonts/TitanOne-OFL.txt +94 -0
  178. package/src/scene/assets/fonts/titan-one.woff2 +0 -0
  179. package/src/scene/assets/gsap/LICENSE.txt +8 -0
  180. package/src/scene/assets/gsap/gsap.min.js +11 -0
  181. package/src/scene/audio.ts +54 -0
  182. package/src/scene/blocks/LICENSE +190 -0
  183. package/src/scene/blocks/NOTICE +5 -0
  184. package/src/scene/blocks/bar-chart-race/bar-chart-race.html +585 -0
  185. package/src/scene/blocks/beat-freeze-cut/beat-freeze-cut.html +874 -0
  186. package/src/scene/blocks/camcorder-hud/camcorder-hud.html +164 -0
  187. package/src/scene/blocks/camera-dolly-zoom/camera-dolly-zoom.html +355 -0
  188. package/src/scene/blocks/code-diff/code-diff.html +817 -0
  189. package/src/scene/blocks/code-snippet-apple-terminal-pro/code-snippet-apple-terminal-pro.html +248 -0
  190. package/src/scene/blocks/code-typing/code-typing.html +764 -0
  191. package/src/scene/blocks/data-chart/data-chart.html +452 -0
  192. package/src/scene/blocks/flowchart-vertical/flowchart-vertical.html +454 -0
  193. package/src/scene/blocks/hw-scribble-transition/hw-scribble-transition.html +153 -0
  194. package/src/scene/blocks/index.json +824 -0
  195. package/src/scene/blocks/lt-accent-underline/lt-accent-underline.html +107 -0
  196. package/src/scene/blocks/lt-bold-block/lt-bold-block.html +109 -0
  197. package/src/scene/blocks/lt-clean-bar/lt-clean-bar.html +112 -0
  198. package/src/scene/blocks/lt-dark-card/lt-dark-card.html +111 -0
  199. package/src/scene/blocks/lt-kicker-name/lt-kicker-name.html +114 -0
  200. package/src/scene/blocks/lt-mask-reveal/lt-mask-reveal.html +120 -0
  201. package/src/scene/blocks/lt-neon-border/lt-neon-border.html +391 -0
  202. package/src/scene/blocks/lt-stack-bars/lt-stack-bars.html +109 -0
  203. package/src/scene/blocks/macos-notification/macos-notification.html +194 -0
  204. package/src/scene/blocks/mk-background/mk-background.html +236 -0
  205. package/src/scene/blocks/mk-callout-highlight/mk-callout-highlight.html +183 -0
  206. package/src/scene/blocks/mk-clone-wall-transition/mk-clone-wall-transition.html +226 -0
  207. package/src/scene/blocks/mk-line-graph/mk-line-graph.html +293 -0
  208. package/src/scene/blocks/mk-progress-stat/mk-progress-stat.html +186 -0
  209. package/src/scene/blocks/mk-specs-list/mk-specs-list.html +223 -0
  210. package/src/scene/blocks/news-ticker/news-ticker.html +409 -0
  211. package/src/scene/blocks/organic-light-leak-overlay/organic-light-leak-overlay.html +126 -0
  212. package/src/scene/blocks/oscilloscope-trace/oscilloscope-trace.html +519 -0
  213. package/src/scene/blocks/snow/snow.html +205 -0
  214. package/src/scene/blocks/split-flap-board/split-flap-board.html +347 -0
  215. package/src/scene/blocks/spotify-card/spotify-card.html +294 -0
  216. package/src/scene/blocks/starfield/starfield.html +188 -0
  217. package/src/scene/blocks/thread-message-stack/thread-message-stack.html +239 -0
  218. package/src/scene/blocks/week-in-merges/week-in-merges.html +470 -0
  219. package/src/scene/blocks/x-post/x-post.html +437 -0
  220. package/src/scene/blocks/yt-comment-card/yt-comment-card.html +319 -0
  221. package/src/scene/blocks/yt-lcd-background/yt-lcd-background.html +307 -0
  222. package/src/scene/blocks/yt-prism-title/yt-prism-title.html +205 -0
  223. package/src/scene/blocks.ts +229 -0
  224. package/src/scene/command.ts +272 -0
  225. package/src/scene/figure.ts +181 -0
  226. package/src/scene/glyphs.ts +107 -0
  227. package/src/scene/init.ts +86 -0
  228. package/src/scene/inspect/css-tree.d.ts +6 -0
  229. package/src/scene/inspect/page-scripts.ts +272 -0
  230. package/src/scene/inspect/report.ts +405 -0
  231. package/src/scene/inspect/run.ts +200 -0
  232. package/src/scene/inspect/source.ts +236 -0
  233. package/src/scene/label.ts +131 -0
  234. package/src/scene/net-guard.ts +74 -0
  235. package/src/scene/render.ts +292 -0
  236. package/src/scene/runtime/browser.ts +199 -0
  237. package/src/scene/runtime/core.ts +181 -0
  238. package/src/scene/runtime/react.tsx +115 -0
  239. package/src/scene/runtime/transitions.ts +133 -0
  240. package/src/scene/sanitize.ts +181 -0
  241. package/src/scene/scene.ts +152 -0
  242. package/src/scene/studio-command.ts +48 -0
  243. package/src/scene/studio-skin.ts +59 -0
  244. package/src/scene/studio.ts +337 -0
  245. package/src/scene/supply.ts +111 -0
  246. package/src/scene/timing.ts +288 -0
  247. package/src/scene/transitions/LICENSE +190 -0
  248. package/src/scene/transitions/NOTICE +5 -0
  249. package/src/scene/transitions/SOURCE.json +24 -0
  250. package/src/scene/transitions/transitions.global.js +38 -0
  251. package/src/scene/transitions.ts +26 -0
  252. package/src/service/all-projects.ts +153 -0
  253. package/src/service/commands.ts +256 -0
  254. package/src/service/due.ts +82 -0
  255. package/src/service/lock.ts +101 -0
  256. package/src/service/log-file.ts +56 -0
  257. package/src/service/state.ts +41 -0
  258. package/src/service/units.ts +90 -0
  259. package/src/service/worker.ts +51 -0
  260. package/src/skill-agents.ts +74 -0
  261. package/src/skill-install.ts +334 -0
  262. package/src/sync-write.ts +21 -0
  263. package/src/tick-report.ts +205 -0
  264. package/src/tts.ts +124 -0
  265. package/vendor/@vfrey/contracts/dist/account.d.ts +97 -0
  266. package/vendor/@vfrey/contracts/dist/account.js +27 -0
  267. package/vendor/@vfrey/contracts/dist/checks.d.ts +8 -0
  268. package/vendor/@vfrey/contracts/dist/checks.js +1 -0
  269. package/vendor/@vfrey/contracts/dist/credentials/crypto.d.ts +4 -0
  270. package/vendor/@vfrey/contracts/dist/credentials/crypto.js +38 -0
  271. package/vendor/@vfrey/contracts/dist/domain/advice-persona.d.ts +10 -0
  272. package/vendor/@vfrey/contracts/dist/domain/advice-persona.js +24 -0
  273. package/vendor/@vfrey/contracts/dist/domain/backoff.d.ts +13 -0
  274. package/vendor/@vfrey/contracts/dist/domain/backoff.js +27 -0
  275. package/vendor/@vfrey/contracts/dist/domain/cadence.d.ts +29 -0
  276. package/vendor/@vfrey/contracts/dist/domain/cadence.js +95 -0
  277. package/vendor/@vfrey/contracts/dist/domain/creators.d.ts +146 -0
  278. package/vendor/@vfrey/contracts/dist/domain/creators.js +2 -0
  279. package/vendor/@vfrey/contracts/dist/domain/disclosure.d.ts +127 -0
  280. package/vendor/@vfrey/contracts/dist/domain/disclosure.js +250 -0
  281. package/vendor/@vfrey/contracts/dist/domain/errors.d.ts +34 -0
  282. package/vendor/@vfrey/contracts/dist/domain/errors.js +68 -0
  283. package/vendor/@vfrey/contracts/dist/domain/redact.d.ts +1 -0
  284. package/vendor/@vfrey/contracts/dist/domain/redact.js +21 -0
  285. package/vendor/@vfrey/contracts/dist/domain/revenue.d.ts +13 -0
  286. package/vendor/@vfrey/contracts/dist/domain/revenue.js +16 -0
  287. package/vendor/@vfrey/contracts/dist/domain/safety.d.ts +20 -0
  288. package/vendor/@vfrey/contracts/dist/domain/safety.js +61 -0
  289. package/vendor/@vfrey/contracts/dist/domain/similarity.d.ts +13 -0
  290. package/vendor/@vfrey/contracts/dist/domain/similarity.js +100 -0
  291. package/vendor/@vfrey/contracts/dist/domain/templates.d.ts +18 -0
  292. package/vendor/@vfrey/contracts/dist/domain/templates.js +67 -0
  293. package/vendor/@vfrey/contracts/dist/domain/tiktok-chunks.d.ts +19 -0
  294. package/vendor/@vfrey/contracts/dist/domain/tiktok-chunks.js +52 -0
  295. package/vendor/@vfrey/contracts/dist/domain/topics.d.ts +78 -0
  296. package/vendor/@vfrey/contracts/dist/domain/topics.js +4 -0
  297. package/vendor/@vfrey/contracts/dist/domain/types.d.ts +216 -0
  298. package/vendor/@vfrey/contracts/dist/domain/types.js +1 -0
  299. package/vendor/@vfrey/contracts/dist/domain/variation.d.ts +57 -0
  300. package/vendor/@vfrey/contracts/dist/domain/variation.js +278 -0
  301. package/vendor/@vfrey/contracts/dist/events.d.ts +20 -0
  302. package/vendor/@vfrey/contracts/dist/events.js +1 -0
  303. package/vendor/@vfrey/contracts/dist/http.d.ts +11 -0
  304. package/vendor/@vfrey/contracts/dist/http.js +37 -0
  305. package/vendor/@vfrey/contracts/dist/index.d.ts +22 -0
  306. package/vendor/@vfrey/contracts/dist/index.js +22 -0
  307. package/vendor/@vfrey/contracts/dist/log.d.ts +2 -0
  308. package/vendor/@vfrey/contracts/dist/log.js +10 -0
  309. package/vendor/@vfrey/contracts/dist/mail.d.ts +55 -0
  310. package/vendor/@vfrey/contracts/dist/mail.js +1 -0
  311. package/vendor/@vfrey/contracts/package.json +18 -0
  312. package/vendor/@vfrey/ingest/dist/index.d.ts +41 -0
  313. package/vendor/@vfrey/ingest/dist/index.js +221 -0
  314. package/vendor/@vfrey/ingest/dist/parse.d.ts +5 -0
  315. package/vendor/@vfrey/ingest/dist/parse.js +165 -0
  316. package/vendor/@vfrey/ingest/package.json +21 -0
  317. package/vendor/framey/LICENSE +190 -0
  318. package/vendor/framey/NOTICE +15 -0
  319. package/vendor/framey/SOURCE.json +24 -0
  320. package/vendor/framey/STUDIO-SOURCE.json +9 -0
  321. package/vendor/framey/bin/framey-localize-fonts.mjs +12 -0
  322. package/vendor/framey/bin/framey.mjs +16 -0
  323. package/vendor/framey/dist/add-VMJD5KBS.js +75 -0
  324. package/vendor/framey/dist/animatedGifPrep-QPKGJ6PM.js +43 -0
  325. package/vendor/framey/dist/api.d.ts +211 -0
  326. package/vendor/framey/dist/api.js +76 -0
  327. package/vendor/framey/dist/appHistory-WW56FZTT.js +31 -0
  328. package/vendor/framey/dist/asset-WQKGOELE.js +26 -0
  329. package/vendor/framey/dist/audio-RSJWVWRA.js +1936 -0
  330. package/vendor/framey/dist/batchRender-3HPICDCT.js +366 -0
  331. package/vendor/framey/dist/beats-HL2BDUZI.js +273 -0
  332. package/vendor/framey/dist/benchmark-RWCWHI5Z.js +230 -0
  333. package/vendor/framey/dist/browser-ECYET3Q2.js +206 -0
  334. package/vendor/framey/dist/bundleWithLocalizedFonts-ALYBQEQT.js +52 -0
  335. package/vendor/framey/dist/captureCompositionFrame-4DA6SEH2.js +94 -0
  336. package/vendor/framey/dist/catalog-5JVDEUWK.js +979 -0
  337. package/vendor/framey/dist/catch-up-NEBCL5NO.js +106 -0
  338. package/vendor/framey/dist/check-F5N544ML.js +111 -0
  339. package/vendor/framey/dist/checkBrowser-LZ66CMJG.js +110 -0
  340. package/vendor/framey/dist/chunk-2K7Z2HVP.js +372 -0
  341. package/vendor/framey/dist/chunk-2UZQHDM7.js +323 -0
  342. package/vendor/framey/dist/chunk-2X4PQEYM.js +34 -0
  343. package/vendor/framey/dist/chunk-3422KMTD.js +168 -0
  344. package/vendor/framey/dist/chunk-3FYD4YEK.js +72 -0
  345. package/vendor/framey/dist/chunk-3GXQPCJC.js +682 -0
  346. package/vendor/framey/dist/chunk-3LM6YPBR.js +179 -0
  347. package/vendor/framey/dist/chunk-442JNXRM.js +20 -0
  348. package/vendor/framey/dist/chunk-4BNN5FGX.js +182 -0
  349. package/vendor/framey/dist/chunk-4P7NX5BM.js +11532 -0
  350. package/vendor/framey/dist/chunk-4UMPIPCQ.js +88 -0
  351. package/vendor/framey/dist/chunk-4YYJOFKS.js +28 -0
  352. package/vendor/framey/dist/chunk-54HJ63Q2.js +29 -0
  353. package/vendor/framey/dist/chunk-5ETJX6WM.js +89 -0
  354. package/vendor/framey/dist/chunk-5SG54JRS.js +68 -0
  355. package/vendor/framey/dist/chunk-634HSJ3J.js +58 -0
  356. package/vendor/framey/dist/chunk-66LREXGS.js +48 -0
  357. package/vendor/framey/dist/chunk-6BEFEWFB.js +25 -0
  358. package/vendor/framey/dist/chunk-6BNTGNGD.js +286 -0
  359. package/vendor/framey/dist/chunk-6DDPOHMK.js +140 -0
  360. package/vendor/framey/dist/chunk-6GV5XSOR.js +214 -0
  361. package/vendor/framey/dist/chunk-6JR2XUEE.js +127 -0
  362. package/vendor/framey/dist/chunk-73QVATIM.js +362 -0
  363. package/vendor/framey/dist/chunk-7FMUTGOH.js +279 -0
  364. package/vendor/framey/dist/chunk-7JQFJ6JJ.js +291 -0
  365. package/vendor/framey/dist/chunk-AHPGAESX.js +113 -0
  366. package/vendor/framey/dist/chunk-AHX34EFP.js +15 -0
  367. package/vendor/framey/dist/chunk-AJ5CSAEZ.js +487 -0
  368. package/vendor/framey/dist/chunk-B7RJV74O.js +5047 -0
  369. package/vendor/framey/dist/chunk-BL4U6U52.js +254 -0
  370. package/vendor/framey/dist/chunk-BSQ7XANM.js +210 -0
  371. package/vendor/framey/dist/chunk-BZ56MTDX.js +6 -0
  372. package/vendor/framey/dist/chunk-C5BO7PMT.js +282 -0
  373. package/vendor/framey/dist/chunk-CFUCHO4T.js +358 -0
  374. package/vendor/framey/dist/chunk-CZGHFATT.js +31 -0
  375. package/vendor/framey/dist/chunk-DLA44CMS.js +5649 -0
  376. package/vendor/framey/dist/chunk-DPVXBVLL.js +31292 -0
  377. package/vendor/framey/dist/chunk-DQBY7FRR.js +397 -0
  378. package/vendor/framey/dist/chunk-EAMLBAHA.js +154 -0
  379. package/vendor/framey/dist/chunk-EOVYQ3IG.js +143 -0
  380. package/vendor/framey/dist/chunk-EVMY7IPL.js +612 -0
  381. package/vendor/framey/dist/chunk-FG4BBFZI.js +122 -0
  382. package/vendor/framey/dist/chunk-FI4I7CME.js +56 -0
  383. package/vendor/framey/dist/chunk-FLREUEHW.js +2081 -0
  384. package/vendor/framey/dist/chunk-FUFP7A7S.js +1126 -0
  385. package/vendor/framey/dist/chunk-FV3PIKGH.js +56 -0
  386. package/vendor/framey/dist/chunk-FXMKHHPO.js +123 -0
  387. package/vendor/framey/dist/chunk-GGZAM5VH.js +1187 -0
  388. package/vendor/framey/dist/chunk-GKZEVJIB.js +39 -0
  389. package/vendor/framey/dist/chunk-GMECXFAN.js +57 -0
  390. package/vendor/framey/dist/chunk-GNEFDLFB.js +621 -0
  391. package/vendor/framey/dist/chunk-GPMLSOEG.js +19274 -0
  392. package/vendor/framey/dist/chunk-GPYVT5EE.js +59 -0
  393. package/vendor/framey/dist/chunk-H6IEMZEV.js +40 -0
  394. package/vendor/framey/dist/chunk-HEFQCIXR.js +170 -0
  395. package/vendor/framey/dist/chunk-HFQ4CWHA.js +118 -0
  396. package/vendor/framey/dist/chunk-HPDTCOKE.js +304 -0
  397. package/vendor/framey/dist/chunk-HTUHQKDB.js +548 -0
  398. package/vendor/framey/dist/chunk-HU2SPFNG.js +103 -0
  399. package/vendor/framey/dist/chunk-HVW7ABXD.js +19 -0
  400. package/vendor/framey/dist/chunk-IG7NDKXO.js +26 -0
  401. package/vendor/framey/dist/chunk-II3J3WXK.js +246 -0
  402. package/vendor/framey/dist/chunk-IR35BCLK.js +13 -0
  403. package/vendor/framey/dist/chunk-ISN5GRYI.js +20 -0
  404. package/vendor/framey/dist/chunk-IUONB55J.js +669 -0
  405. package/vendor/framey/dist/chunk-IVSUQR2B.js +161 -0
  406. package/vendor/framey/dist/chunk-IXU6PQSW.js +259 -0
  407. package/vendor/framey/dist/chunk-IZCW2NKT.js +29 -0
  408. package/vendor/framey/dist/chunk-JGXJU5PF.js +68 -0
  409. package/vendor/framey/dist/chunk-JHOCK3DG.js +27 -0
  410. package/vendor/framey/dist/chunk-JVBRJMOP.js +121 -0
  411. package/vendor/framey/dist/chunk-K4MJX7SR.js +17 -0
  412. package/vendor/framey/dist/chunk-K4WKGXRC.js +18 -0
  413. package/vendor/framey/dist/chunk-KGPLIH7B.js +127 -0
  414. package/vendor/framey/dist/chunk-KLIKCX6Q.js +63 -0
  415. package/vendor/framey/dist/chunk-KXTKKBOC.js +61 -0
  416. package/vendor/framey/dist/chunk-KZI44OPA.js +185 -0
  417. package/vendor/framey/dist/chunk-LEKZNRKI.js +45 -0
  418. package/vendor/framey/dist/chunk-LFGJWOVA.js +61 -0
  419. package/vendor/framey/dist/chunk-LLMSX6V6.js +17 -0
  420. package/vendor/framey/dist/chunk-LTGTN2TH.js +195 -0
  421. package/vendor/framey/dist/chunk-M2WBOQ2Q.js +54 -0
  422. package/vendor/framey/dist/chunk-MI5FTLYK.js +399 -0
  423. package/vendor/framey/dist/chunk-MLQP2QUF.js +1809 -0
  424. package/vendor/framey/dist/chunk-MMQ52NIZ.js +956 -0
  425. package/vendor/framey/dist/chunk-ND46XKK3.js +742 -0
  426. package/vendor/framey/dist/chunk-NYEXNSHM.js +504 -0
  427. package/vendor/framey/dist/chunk-O4YRB27L.js +57 -0
  428. package/vendor/framey/dist/chunk-O7BGSQ5F.js +92 -0
  429. package/vendor/framey/dist/chunk-OFQWTKPW.js +117 -0
  430. package/vendor/framey/dist/chunk-P5ZBLXJ6.js +91 -0
  431. package/vendor/framey/dist/chunk-P6LJ4Y32.js +4662 -0
  432. package/vendor/framey/dist/chunk-PKZ7RK5N.js +281 -0
  433. package/vendor/framey/dist/chunk-PURHAXFL.js +15 -0
  434. package/vendor/framey/dist/chunk-Q5X2B5WT.js +29 -0
  435. package/vendor/framey/dist/chunk-QAMKOG67.js +248 -0
  436. package/vendor/framey/dist/chunk-R6YBK4XX.js +514 -0
  437. package/vendor/framey/dist/chunk-RGLQURIX.js +2997 -0
  438. package/vendor/framey/dist/chunk-RNV3DM4S.js +3573 -0
  439. package/vendor/framey/dist/chunk-ROEW726S.js +4638 -0
  440. package/vendor/framey/dist/chunk-RQVDTPGD.js +6 -0
  441. package/vendor/framey/dist/chunk-RVZHGOGR.js +26 -0
  442. package/vendor/framey/dist/chunk-RXATE6FZ.js +385 -0
  443. package/vendor/framey/dist/chunk-S3S4TGEH.js +424 -0
  444. package/vendor/framey/dist/chunk-SDYDNHEE.js +11202 -0
  445. package/vendor/framey/dist/chunk-SSWBU67I.js +173 -0
  446. package/vendor/framey/dist/chunk-ST35C4K6.js +28 -0
  447. package/vendor/framey/dist/chunk-SZNTYTIV.js +990 -0
  448. package/vendor/framey/dist/chunk-T2EUWCJE.js +29 -0
  449. package/vendor/framey/dist/chunk-T6AGUTZJ.js +444 -0
  450. package/vendor/framey/dist/chunk-THJALHFJ.js +11131 -0
  451. package/vendor/framey/dist/chunk-TIQB63MX.js +18128 -0
  452. package/vendor/framey/dist/chunk-U242RTCT.js +134 -0
  453. package/vendor/framey/dist/chunk-UJZREAV6.js +3901 -0
  454. package/vendor/framey/dist/chunk-UMD3V7CU.js +264 -0
  455. package/vendor/framey/dist/chunk-UOFAE5RF.js +42 -0
  456. package/vendor/framey/dist/chunk-UTEIUGAZ.js +29 -0
  457. package/vendor/framey/dist/chunk-VAS6HQX3.js +138 -0
  458. package/vendor/framey/dist/chunk-VC76R6W2.js +27 -0
  459. package/vendor/framey/dist/chunk-VFZ7W5W7.js +226 -0
  460. package/vendor/framey/dist/chunk-VHNLZAE2.js +348 -0
  461. package/vendor/framey/dist/chunk-VMC4WSH5.js +278 -0
  462. package/vendor/framey/dist/chunk-VORQIH64.js +108 -0
  463. package/vendor/framey/dist/chunk-VTXFRIHO.js +186 -0
  464. package/vendor/framey/dist/chunk-W2LGJSEN.js +182 -0
  465. package/vendor/framey/dist/chunk-WKBM7NDG.js +32 -0
  466. package/vendor/framey/dist/chunk-WMQNIQDJ.js +2057 -0
  467. package/vendor/framey/dist/chunk-WN4XZC5F.js +504 -0
  468. package/vendor/framey/dist/chunk-WQ6ZXHFQ.js +133 -0
  469. package/vendor/framey/dist/chunk-X7HBSTCZ.js +53 -0
  470. package/vendor/framey/dist/chunk-X7MTZ4T2.js +6 -0
  471. package/vendor/framey/dist/chunk-XPTOJZJF.js +308 -0
  472. package/vendor/framey/dist/chunk-XT6MXC7A.js +6 -0
  473. package/vendor/framey/dist/chunk-XT7X6QCP.js +6 -0
  474. package/vendor/framey/dist/chunk-XTMDWNEC.js +2820 -0
  475. package/vendor/framey/dist/chunk-XUHOIDKG.js +6 -0
  476. package/vendor/framey/dist/chunk-XX7LTW5Y.js +138 -0
  477. package/vendor/framey/dist/chunk-Y3YGTZEG.js +382 -0
  478. package/vendor/framey/dist/chunk-Y5SBSGN7.js +346 -0
  479. package/vendor/framey/dist/chunk-YCMVI4MZ.js +625 -0
  480. package/vendor/framey/dist/chunk-YOR4CTCE.js +261 -0
  481. package/vendor/framey/dist/chunk-YPRBGITP.js +2748 -0
  482. package/vendor/framey/dist/chunk-ZKDMAOFU.js +32 -0
  483. package/vendor/framey/dist/chunk-ZW6DURPF.js +1318 -0
  484. package/vendor/framey/dist/clean-ZJOGDKDM.js +336 -0
  485. package/vendor/framey/dist/cli.js +439 -0
  486. package/vendor/framey/dist/cloudrun-HGJ3B6ZV.js +767 -0
  487. package/vendor/framey/dist/commands/contrast-audit.browser.js +643 -0
  488. package/vendor/framey/dist/commands/layout-audit.browser.js +2914 -0
  489. package/vendor/framey/dist/commands/motion-sample.browser.js +74 -0
  490. package/vendor/framey/dist/commands/motion-signature.browser.js +475 -0
  491. package/vendor/framey/dist/compare-W7UY3BAZ.js +463 -0
  492. package/vendor/framey/dist/compiler-TM3W6GYF.js +136 -0
  493. package/vendor/framey/dist/component-QF5VGYMU.js +176 -0
  494. package/vendor/framey/dist/compositions-XT3PMNDF.js +238 -0
  495. package/vendor/framey/dist/contactSheet-7W6GYHBQ.js +24 -0
  496. package/vendor/framey/dist/deploy-6FJPWKF2.js +299 -0
  497. package/vendor/framey/dist/destroy-HE2RIPKZ.js +57 -0
  498. package/vendor/framey/dist/deterministicFonts-RADD3BSX.js +50 -0
  499. package/vendor/framey/dist/dist-4S2FGGVY.js +422 -0
  500. package/vendor/framey/dist/dist-BE4B2EZE.js +32 -0
  501. package/vendor/framey/dist/dist-EWXJGN63.js +129 -0
  502. package/vendor/framey/dist/docs-VKIRFEEX.js +131 -0
  503. package/vendor/framey/dist/doctor-2UNJ2G7U.js +74 -0
  504. package/vendor/framey/dist/dom-37K6YOH4.js +14 -0
  505. package/vendor/framey/dist/dotEnv-7PZQTVIM.js +54 -0
  506. package/vendor/framey/dist/events-ACY3P2PA.js +76 -0
  507. package/vendor/framey/dist/events-IHB3B2MK.js +69 -0
  508. package/vendor/framey/dist/ffmpeg-DR5LGGSK.js +63 -0
  509. package/vendor/framey/dist/figma-BODVUPHB.js +63 -0
  510. package/vendor/framey/dist/fontData.generated-NXAOI4GH.js +13 -0
  511. package/vendor/framey/dist/fontLocalizeCli.js +142 -0
  512. package/vendor/framey/dist/gpuPolicy-XLEXORZY.js +49 -0
  513. package/vendor/framey/dist/grade-compare-GAXFQ74C.js +661 -0
  514. package/vendor/framey/dist/gsapParser-IEUSLYDJ.js +2885 -0
  515. package/vendor/framey/dist/gsapParserAcorn-LZJKCFQI.js +30 -0
  516. package/vendor/framey/dist/help-VCB5X2HS.js +184 -0
  517. package/vendor/framey/dist/history-ENGZEVFT.js +471 -0
  518. package/vendor/framey/dist/hyperframe.manifest.json +22 -0
  519. package/vendor/framey/dist/hyperframe.runtime.iife.js +1132 -0
  520. package/vendor/framey/dist/hyperframes-player.global.js +478 -0
  521. package/vendor/framey/dist/hyperframes-slideshow.global.js +148 -0
  522. package/vendor/framey/dist/info-WPCJHBMU.js +182 -0
  523. package/vendor/framey/dist/init-G4WGLYGD.js +1176 -0
  524. package/vendor/framey/dist/inspect-5IQ6CNEQ.js +106 -0
  525. package/vendor/framey/dist/installer-ML4HWUV7.js +40 -0
  526. package/vendor/framey/dist/keyframes-Q6XJO7VW.js +2755 -0
  527. package/vendor/framey/dist/lambda-A7JC74JF.js +468 -0
  528. package/vendor/framey/dist/layout-YET6XNXA.js +95 -0
  529. package/vendor/framey/dist/lint-I7FZN2VJ.js +162 -0
  530. package/vendor/framey/dist/lintProject-PDB3UN72.js +30 -0
  531. package/vendor/framey/dist/manager-CD6LJ7ZN.js +45 -0
  532. package/vendor/framey/dist/manager-WA4F2GST.js +67 -0
  533. package/vendor/framey/dist/media-treatment-BXYS4WVA.js +1030 -0
  534. package/vendor/framey/dist/media-use-LWH3MANK.js +113 -0
  535. package/vendor/framey/dist/models-K363FYM4.js +195 -0
  536. package/vendor/framey/dist/motionShot-GPAVWXLH.js +640 -0
  537. package/vendor/framey/dist/normalize-FASIPBUM.js +24 -0
  538. package/vendor/framey/dist/normalize-audio-VDWYRHN6.js +268 -0
  539. package/vendor/framey/dist/open-E3TB6P6J.js +128 -0
  540. package/vendor/framey/dist/parakeetRunner-UP34UOI2.js +58 -0
  541. package/vendor/framey/dist/pipeline-VXIUYEBU.js +64 -0
  542. package/vendor/framey/dist/play-BUTDQ4DV.js +353 -0
  543. package/vendor/framey/dist/policies-OLDYGFK5.js +309 -0
  544. package/vendor/framey/dist/preflight-2KZ2GWAZ.js +55 -0
  545. package/vendor/framey/dist/present-GXYSM7NX.js +353 -0
  546. package/vendor/framey/dist/preview-EKZ33JOJ.js +1910 -0
  547. package/vendor/framey/dist/producer-SKU5EV3J.js +13 -0
  548. package/vendor/framey/dist/progress-ZWB4Z4ET.js +76 -0
  549. package/vendor/framey/dist/projectConfig-3MZAYVTS.js +60 -0
  550. package/vendor/framey/dist/publication-DTKWNF74.js +33 -0
  551. package/vendor/framey/dist/remove-background-OY5L7XN2.js +265 -0
  552. package/vendor/framey/dist/render-CDV4GDFX.js +187 -0
  553. package/vendor/framey/dist/render-YE5YAMOD.js +2620 -0
  554. package/vendor/framey/dist/render-batch-ZAMFPCLL.js +274 -0
  555. package/vendor/framey/dist/renderSetupWorker.js +120 -0
  556. package/vendor/framey/dist/resolver-D5BIPSEW.js +42 -0
  557. package/vendor/framey/dist/runtimeVersion.js +18 -0
  558. package/vendor/framey/dist/sam-WFZXI4F7.js +23 -0
  559. package/vendor/framey/dist/shaderTransitionWorker.js +44 -0
  560. package/vendor/framey/dist/sherpa-RQWLV7I3.js +79 -0
  561. package/vendor/framey/dist/sherpaWorker.js +84 -0
  562. package/vendor/framey/dist/sites-AMOVABTK.js +54 -0
  563. package/vendor/framey/dist/skill-ZHVLH7KX.js +15 -0
  564. package/vendor/framey/dist/slideshow-SJGK4RQS.js +24 -0
  565. package/vendor/framey/dist/snapshot-TUXL6LBQ.js +1515 -0
  566. package/vendor/framey/dist/src-JHQGB2S6.js +215 -0
  567. package/vendor/framey/dist/src-Z5PGEMXB.js +532 -0
  568. package/vendor/framey/dist/state-B6H3WHCF.js +28 -0
  569. package/vendor/framey/dist/staticProjectServer-DFVZOUGI.js +63 -0
  570. package/vendor/framey/dist/studio/assets/hyperframes-player-DLaKjw0L.js +478 -0
  571. package/vendor/framey/dist/studio/assets/index-BX3KHhGX.js +71 -0
  572. package/vendor/framey/dist/studio/assets/index-CYbvOVp-.css +1 -0
  573. package/vendor/framey/dist/studio/assets/index-Dp3QEUGL.js +402 -0
  574. package/vendor/framey/dist/studio/assets/index-qrTAXbqc.js +531 -0
  575. package/vendor/framey/dist/studio/favicon.svg +14 -0
  576. package/vendor/framey/dist/studio/icons/timeline/audio.svg +7 -0
  577. package/vendor/framey/dist/studio/icons/timeline/captions.svg +5 -0
  578. package/vendor/framey/dist/studio/icons/timeline/composition.svg +12 -0
  579. package/vendor/framey/dist/studio/icons/timeline/image.svg +18 -0
  580. package/vendor/framey/dist/studio/icons/timeline/music.svg +10 -0
  581. package/vendor/framey/dist/studio/icons/timeline/text.svg +3 -0
  582. package/vendor/framey/dist/studio/index.html +23 -0
  583. package/vendor/framey/dist/studio-api-2NECGRMB.js +179 -0
  584. package/vendor/framey/dist/studioSelectionClient-IUUPKUHZ.js +25 -0
  585. package/vendor/framey/dist/studioServer-N2OPUMOQ.js +1569 -0
  586. package/vendor/framey/dist/synthesize-PKTB5ATG.js +183 -0
  587. package/vendor/framey/dist/system-7F3UEHQT.js +27 -0
  588. package/vendor/framey/dist/telemetry-DQGPEKM6.js +145 -0
  589. package/vendor/framey/dist/telemetry-UN55N6U6.js +77 -0
  590. package/vendor/framey/dist/timeline-LHUUMK63.js +1295 -0
  591. package/vendor/framey/dist/tokens-OLECWDFK.js +92 -0
  592. package/vendor/framey/dist/transcribe-O7HQKMIV.js +331 -0
  593. package/vendor/framey/dist/transcribe-UJESPBT5.js +76 -0
  594. package/vendor/framey/dist/tts-YCCQHZD4.js +234 -0
  595. package/vendor/framey/dist/urlDownloader-H2LXLG4U.js +48 -0
  596. package/vendor/framey/dist/usage-HG4WMM7A.js +2167 -0
  597. package/vendor/framey/dist/validate-F2XMN6EV.js +87 -0
  598. package/vendor/framey/dist/vendor/npm/gsap@3.15.0/GSAP-LICENSE.txt +8 -0
  599. package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/CustomEase.min.js +11 -0
  600. package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/MotionPathPlugin.min.js +11 -0
  601. package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/gsap.min.js +11 -0
  602. package/vendor/framey/dist/warmCheck.js +220 -0
  603. package/vendor/framey/dist/warmCheckClient.js +139 -0
  604. package/vendor/framey/dist/workspaceBuildCheck-XYD2YTFW.js +133 -0
  605. package/vendor/framey-player/LICENSE +190 -0
  606. package/vendor/framey-player/NOTICE +16 -0
  607. package/vendor/framey-player/SOURCE.json +6 -0
  608. package/vendor/framey-player/hyperframes-player.global.js +478 -0
  609. package/vendor/framey.pin +1 -0
  610. package/bin/index.js +0 -3
@@ -0,0 +1,59 @@
1
+ /**
2
+ * vfrey's skin for Framey's studio. The studio is Framey's (Apache-2.0, see vendor/framey/LICENSE and NOTICE), built for
3
+ * HeyGen's HyperFrames product. vfrey does not ship their logo, their desktop-app upsell, or controls that need
4
+ * features vfrey does not have, so the front server adds this stylesheet and script to the studio page. They work on the
5
+ * rendered DOM, so a re-sync of a newer studio build stays clean without patching minified code.
6
+ *
7
+ * Hidden: the logo (replaced by a plain wordmark), "Install the desktop app", Export and the Renders panel (a render
8
+ * goes through `npx vfrey render --scene DIR`, which supplies the timing and voice and burns in the AI label, and
9
+ * the studio's own render would skip all three), the Catalog panel (it loads Framey's hosted block registry; vfrey's
10
+ * blocks are `npx vfrey scene add`), and "Describe a change to the agent" (a hand-off to their desktop app).
11
+ * Kept: Capture (a local PNG of the current frame), the preview, the timeline, the code editor, the inspector.
12
+ */
13
+ export const SKIN_PATH = "/__vfrey/studio";
14
+
15
+ export const SKIN_CSS = `
16
+ svg[aria-label="Framey"] { display: none !important; }
17
+ .vfrey-wordmark { font: 600 15px/1 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; letter-spacing: -0.01em; white-space: nowrap; }
18
+ [data-testid="header-open-in-desktop"],
19
+ [data-testid="header-export"],
20
+ [role="tab"][data-tab-panel-id="catalog"],
21
+ [role="tab"][data-tab-panel-id="renders"],
22
+ .vfrey-hidden { display: none !important; }
23
+ `;
24
+
25
+ export const SKIN_JS = `(() => {
26
+ const WORD = "vfrey studio";
27
+ const apply = () => {
28
+ if (document.title !== WORD) document.title = WORD;
29
+ for (const logo of document.querySelectorAll('svg[aria-label="Framey"]')) {
30
+ if (!(logo.previousElementSibling && logo.previousElementSibling.classList.contains("vfrey-wordmark"))) {
31
+ const word = document.createElement("span");
32
+ word.className = "vfrey-wordmark";
33
+ word.textContent = WORD;
34
+ logo.before(word);
35
+ }
36
+ }
37
+ for (const el of document.querySelectorAll("span")) {
38
+ if (el.childElementCount <= 1 && el.textContent.trim().endsWith("Describe a change to the agent")) {
39
+ const row = el.parentElement;
40
+ if (row) row.classList.add("vfrey-hidden");
41
+ }
42
+ }
43
+ };
44
+ let queued = false;
45
+ const later = () => {
46
+ if (queued) return;
47
+ queued = true;
48
+ requestAnimationFrame(() => { queued = false; apply(); });
49
+ };
50
+ new MutationObserver(later).observe(document.documentElement, { childList: true, subtree: true });
51
+ apply();
52
+ })();
53
+ `;
54
+
55
+ /** The studio page with the skin linked in. */
56
+ export function skinHtml(html: string): string {
57
+ const tags = `<link rel="stylesheet" href="${SKIN_PATH}.css"><script src="${SKIN_PATH}.js" defer></script>`;
58
+ return /<\/head>/i.test(html) ? html.replace(/<\/head>/i, `${tags}</head>`) : `${tags}${html}`;
59
+ }
@@ -0,0 +1,337 @@
1
+ import { spawn, type ChildProcess } from "node:child_process";
2
+ import { existsSync, statSync } from "node:fs";
3
+ import { copyFile, mkdtemp, readFile, rm } from "node:fs/promises";
4
+ import http, { type IncomingMessage, type Server, type ServerResponse } from "node:http";
5
+ import net from "node:net";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+ import { FRAMEY_ENTRY } from "../product/probes.js";
9
+ import { resolveHeadlessShell } from "../render/chrome.js";
10
+ import { linesToBeats, scriptTemplate } from "../generate/script.js";
11
+ import { SceneError, storeScene } from "./scene.js";
12
+ import { SUPPLY_DIR, writeSupply } from "./supply.js";
13
+ import { estimateTiming, type SceneTiming } from "./timing.js";
14
+ import { sceneTimingFor } from "./audio.js";
15
+ import { SKIN_CSS, SKIN_JS, SKIN_PATH, skinHtml } from "./studio-skin.js";
16
+ import type { BeatInput } from "./timing.js";
17
+
18
+ type Env = Record<string, string | undefined>;
19
+
20
+ /** Where a studio looks for a free port first. Away from the dashboard's range and from 3012. */
21
+ export const STUDIO_PORT_START = 47000;
22
+ const PORT_SCAN = 200;
23
+
24
+ /** Ports a studio never picks by itself: the dev dashboard (3012) and the test dashboards (4300 to 4391). */
25
+ export function reservedPort(port: number): boolean {
26
+ return port === 3012 || (port >= 4300 && port <= 4391);
27
+ }
28
+
29
+ /** The studio SPA Framey serves, put into the package by scripts/sync-framey-studio.mjs. */
30
+ export const STUDIO_BUNDLE = path.join(path.dirname(FRAMEY_ENTRY), "..", "dist", "studio", "index.html");
31
+
32
+ export interface StudioArgs {
33
+ dir: string;
34
+ port: number | null;
35
+ noBrowser: boolean;
36
+ }
37
+
38
+ export type StudioArgsResult = { ok: true; args: StudioArgs } | { ok: false; message: string; fix: string };
39
+
40
+ /** `scene studio DIR [--port N] [--no-browser]`; `argv` is what follows `studio`. */
41
+ export function parseStudioArgs(argv: string[]): StudioArgsResult {
42
+ let dir: string | null = null;
43
+ let port: number | null = null;
44
+ let noBrowser = false;
45
+ for (let i = 0; i < argv.length; i += 1) {
46
+ const arg = argv[i]!;
47
+ if (arg === "--no-browser") noBrowser = true;
48
+ else if (arg === "--port" || arg.startsWith("--port=")) {
49
+ const raw = arg === "--port" ? argv[(i += 1)] : arg.slice("--port=".length);
50
+ if (raw === undefined || !/^\d+$/.test(raw) || Number(raw) < 1024 || Number(raw) > 65535) {
51
+ return { ok: false, message: `--port needs a number from 1024 to 65535, not "${raw ?? ""}".`, fix: "for example `--port 47123`, or leave it out and vfrey picks a free port." };
52
+ }
53
+ port = Number(raw);
54
+ } else if (arg.startsWith("--")) {
55
+ return { ok: false, message: `scene studio does not know ${arg}.`, fix: "use `npx vfrey scene studio DIR [--port N] [--no-browser]`." };
56
+ } else if (dir === null) dir = arg;
57
+ else return { ok: false, message: `scene studio takes one folder, and got a second: ${arg}.`, fix: "pass just the scene folder." };
58
+ }
59
+ if (dir === null) return { ok: false, message: "scene studio needs a scene folder.", fix: "run `npx vfrey scene studio my-scene` (`npx vfrey scene init my-scene` writes one)." };
60
+ return { ok: true, args: { dir: path.resolve(dir), port, noBrowser } };
61
+ }
62
+
63
+ function freeOn(port: number, host: string): Promise<boolean> {
64
+ return new Promise((resolve) => {
65
+ const probe = net.createServer();
66
+ probe.unref();
67
+ probe.once("error", (error: NodeJS.ErrnoException) => resolve(error.code !== "EADDRINUSE" && error.code !== "EACCES"));
68
+ probe.listen({ port, host }, () => probe.close(() => resolve(true)));
69
+ });
70
+ }
71
+
72
+ /** The first free loopback port from `start`, never a reserved one. Null when none of the next 200 is free. */
73
+ export async function pickStudioPort(start = STUDIO_PORT_START, isFree: (port: number) => Promise<boolean> = (p) => freeOn(p, "127.0.0.1")): Promise<number | null> {
74
+ for (let port = start; port < start + PORT_SCAN && port <= 65535; port += 1) {
75
+ if (reservedPort(port)) continue;
76
+ if (await isFree(port)) return port;
77
+ }
78
+ return null;
79
+ }
80
+
81
+ /**
82
+ * What the studio and the scene it shows may load: only this origin. No request leaves the machine, whatever the scene
83
+ * says (the same rule as the network block while a scene renders). `unsafe-inline` and `unsafe-eval` are for the
84
+ * scene's own scripts and the studio's code editor; `connect-src 'self'` and `default-src 'self'` are what keep a
85
+ * scene from calling out.
86
+ */
87
+ export const STUDIO_CSP = [
88
+ "default-src 'self'",
89
+ "script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:",
90
+ "style-src 'self' 'unsafe-inline'",
91
+ "img-src 'self' data: blob:",
92
+ "media-src 'self' data: blob:",
93
+ "font-src 'self' data:",
94
+ "connect-src 'self'",
95
+ "frame-src 'self' blob:",
96
+ "worker-src 'self' blob:",
97
+ "object-src 'none'",
98
+ "base-uri 'self'",
99
+ "form-action 'none'",
100
+ "frame-ancestors 'self'",
101
+ ].join("; ");
102
+
103
+ /** True when the Host header names this studio's own loopback address (a rebound hostname does not). */
104
+ export function hostAllowed(host: string | undefined, port: number): boolean {
105
+ return host === `127.0.0.1:${port}` || host === `localhost:${port}`;
106
+ }
107
+
108
+ /** Writes are allowed from the studio page itself only: no foreign Origin, no cross-site fetch. */
109
+ export function writeAllowed(headers: http.IncomingHttpHeaders, port: number): boolean {
110
+ const origin = headers.origin;
111
+ if (origin && origin !== `http://127.0.0.1:${port}` && origin !== `http://localhost:${port}`) return false;
112
+ const site = headers["sec-fetch-site"];
113
+ return !site || site === "same-origin" || site === "none";
114
+ }
115
+
116
+ function refuse(res: ServerResponse, status: number, message: string): void {
117
+ res.writeHead(status, { "content-type": "text/plain; charset=utf-8", "content-security-policy": STUDIO_CSP });
118
+ res.end(`${message}\n`);
119
+ }
120
+
121
+ /** The face of the studio: a loopback server that checks Host and Origin, adds the CSP, and passes everything to Framey. */
122
+ export function frontServer(publicPort: number, innerPort: number): Server {
123
+ const server = http.createServer((req: IncomingMessage, res: ServerResponse) => {
124
+ if (!hostAllowed(req.headers.host, publicPort)) return refuse(res, 403, `Unknown host. Open the studio at http://127.0.0.1:${publicPort}.`);
125
+ const method = req.method ?? "GET";
126
+ if (method !== "GET" && method !== "HEAD" && !writeAllowed(req.headers, publicPort)) return refuse(res, 403, "Writes are accepted from the studio page only.");
127
+ const pathname = (req.url ?? "/").split("?")[0]!;
128
+ if (method === "GET" && (pathname === `${SKIN_PATH}.css` || pathname === `${SKIN_PATH}.js`)) {
129
+ const css = pathname.endsWith(".css");
130
+ res.writeHead(200, { "content-type": css ? "text/css; charset=utf-8" : "text/javascript; charset=utf-8", "content-security-policy": STUDIO_CSP, "cache-control": "no-store", "x-content-type-options": "nosniff" });
131
+ return void res.end(css ? SKIN_CSS : SKIN_JS);
132
+ }
133
+ const headers = { ...req.headers, host: `127.0.0.1:${innerPort}` };
134
+ if (headers.origin) headers.origin = `http://127.0.0.1:${innerPort}`;
135
+ delete headers.referer;
136
+ delete headers["accept-encoding"];
137
+ const upstream = http.request({ host: "127.0.0.1", port: innerPort, method, path: req.url, headers }, (back) => {
138
+ const out = { ...back.headers };
139
+ delete out["content-security-policy"];
140
+ out["content-security-policy"] = STUDIO_CSP;
141
+ out["x-content-type-options"] = "nosniff";
142
+ out["referrer-policy"] = "no-referrer";
143
+ // The studio's own page (not the scene's preview, which is under /api) gets vfrey's skin.
144
+ if (method === "GET" && !pathname.startsWith("/api/") && String(back.headers["content-type"] ?? "").startsWith("text/html")) {
145
+ const chunks: Buffer[] = [];
146
+ back.on("data", (c: Buffer) => chunks.push(c));
147
+ back.on("end", () => {
148
+ const html = skinHtml(Buffer.concat(chunks).toString("utf8"));
149
+ delete out["content-length"];
150
+ delete out["content-encoding"];
151
+ delete out.etag;
152
+ delete out["transfer-encoding"];
153
+ res.writeHead(back.statusCode ?? 502, { ...out, "content-length": String(Buffer.byteLength(html)) });
154
+ res.end(html);
155
+ });
156
+ return;
157
+ }
158
+ res.writeHead(back.statusCode ?? 502, out);
159
+ back.pipe(res);
160
+ });
161
+ upstream.on("error", () => (res.headersSent ? res.destroy() : refuse(res, 502, "The studio's editor process is not answering.")));
162
+ res.on("close", () => upstream.destroy());
163
+ req.pipe(upstream);
164
+ });
165
+ server.on("upgrade", (_req, socket) => socket.destroy());
166
+ return server;
167
+ }
168
+
169
+ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
170
+
171
+ async function waitUntilUp(port: number, child: ChildProcess, output: () => string, timeoutMs: number): Promise<void> {
172
+ const until = Date.now() + timeoutMs;
173
+ while (Date.now() < until) {
174
+ if (child.exitCode !== null) throw new Error(`the editor process stopped: ${output().trim().split("\n").slice(-3).join(" ").slice(0, 300) || `exit code ${child.exitCode}`}`);
175
+ try {
176
+ const res = await fetch(`http://127.0.0.1:${port}/api/projects`, { signal: AbortSignal.timeout(1500) });
177
+ if (res.ok) return;
178
+ } catch {
179
+ /* not listening yet */
180
+ }
181
+ await sleep(120);
182
+ }
183
+ throw new Error("the editor process did not start in time");
184
+ }
185
+
186
+ export interface StudioHandle {
187
+ /** The address to open: the studio page with the scene's project selected. */
188
+ url: string;
189
+ port: number;
190
+ dir: string;
191
+ close(): Promise<void>;
192
+ }
193
+
194
+ export interface StartStudioInput {
195
+ dir: string;
196
+ port?: number | null;
197
+ env?: Env;
198
+ /** Timing and voice to supply, when the scene belongs to a run. Without them the scene's own `vfrey/` files are kept. */
199
+ supply?: { title: string; lines: string[]; beats: BeatInput[]; voiceFile: string | null };
200
+ timeoutMs?: number;
201
+ }
202
+
203
+ /** The folder must be an HTML scene. A React scene or a folder without one is a ✗ with a Fix. */
204
+ export async function checkStudioScene(dir: string): Promise<void> {
205
+ const info = (() => {
206
+ try {
207
+ return statSync(dir);
208
+ } catch {
209
+ return null;
210
+ }
211
+ })();
212
+ if (!info?.isDirectory()) {
213
+ throw new SceneError(`The scene folder ${dir} does not exist.`, `write ${path.join(dir, "index.html")}, or run \`npx vfrey scene init ${dir}\` for a starting point.`, "scene_missing");
214
+ }
215
+ if (!existsSync(path.join(dir, "index.html"))) {
216
+ if (existsSync(path.join(dir, "index.tsx"))) {
217
+ throw new SceneError(
218
+ `The scene in ${dir} is a Remotion scene (index.tsx). The studio edits HTML scenes only.`,
219
+ `edit index.tsx in your own editor and render it with \`npx vfrey render --scene ${dir} --engine remotion\`, or write an HTML scene (\`npx vfrey scene init DIR\`) to use the studio.`,
220
+ "scene_studio_remotion",
221
+ );
222
+ }
223
+ throw new SceneError(`The scene folder ${dir} has no index.html.`, `write ${path.join(dir, "index.html")}, or run \`npx vfrey scene init ${dir}\` for a starting point.`, "scene_missing");
224
+ }
225
+ if (!existsSync(STUDIO_BUNDLE)) {
226
+ throw new SceneError("The studio is missing from this install.", "reinstall vfrey (`npm i -g @vfrey/vfrey`), or run `npm run sync:framey-studio` in a source checkout.", "studio_missing");
227
+ }
228
+ }
229
+
230
+ /** Writes the files vfrey supplies at render time into the scene folder, so the HTML opens the way a render would see it. */
231
+ export async function supplyForStudio(dir: string, input: StartStudioInput["supply"], env: Env): Promise<void> {
232
+ let timing: SceneTiming | null = null;
233
+ let voiceFile: string | null = null;
234
+ let keep: string | null = null;
235
+ try {
236
+ if (input) {
237
+ timing = await sceneTimingFor({ title: input.title, lines: input.lines, beats: input.beats }, input.voiceFile, env);
238
+ voiceFile = input.voiceFile;
239
+ } else {
240
+ try {
241
+ timing = JSON.parse(await readFile(path.join(dir, SUPPLY_DIR, "timing.json"), "utf8")) as SceneTiming;
242
+ } catch {
243
+ timing = null;
244
+ }
245
+ const own = path.join(dir, SUPPLY_DIR, "voice.wav");
246
+ if (existsSync(own)) {
247
+ keep = await mkdtemp(path.join(os.tmpdir(), "vfrey-studio-voice-"));
248
+ voiceFile = path.join(keep, "voice.wav");
249
+ await copyFile(own, voiceFile);
250
+ }
251
+ }
252
+ if (!timing) {
253
+ const sample = JSON.parse(scriptTemplate()) as { title: string; lines: string[] };
254
+ timing = estimateTiming({ title: sample.title, lines: sample.lines, beats: linesToBeats(sample.lines).map((b) => ({ role: b.role, text: b.text, caption: b.caption })) });
255
+ }
256
+ await writeSupply({ dir, timing, voiceFile, kind: "html", env });
257
+ } finally {
258
+ if (keep) await rm(keep, { recursive: true, force: true });
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Starts the studio for a scene folder: supplies the vfrey files, runs the vendored Framey editor on a private loopback
264
+ * port, and puts a guard in front of it on the port the person opens. Edits are saved by the editor straight into `dir`.
265
+ */
266
+ export async function startStudio(input: StartStudioInput): Promise<StudioHandle> {
267
+ const env = input.env ?? process.env;
268
+ const dir = path.resolve(input.dir);
269
+ await checkStudioScene(dir);
270
+ await supplyForStudio(dir, input.supply, env);
271
+ const port = input.port ?? (await pickStudioPort());
272
+ if (port === null || port === undefined) throw new SceneError("No free port was found for the studio.", "free a port from 47000 up, or pass --port N.", "studio_no_port");
273
+ if (!(await freeOn(port, "127.0.0.1"))) throw new SceneError(`Port ${port} is in use.`, "pass another --port N, or leave it out and vfrey picks a free one.", "studio_port_busy");
274
+ const innerPort = await pickStudioPort(port + 1, (p) => freeOn(p, "127.0.0.1"));
275
+ if (innerPort === null) throw new SceneError("No free port was found for the studio's editor process.", "free a port from 47000 up.", "studio_no_port");
276
+ const chrome = resolveHeadlessShell(env, process.cwd());
277
+ const childEnv = {
278
+ ...env,
279
+ HYPERFRAMES_PREVIEW_HOST: "127.0.0.1",
280
+ HYPERFRAMES_NO_TELEMETRY: "1",
281
+ HYPERFRAMES_NO_UPDATE_CHECK: "1",
282
+ DO_NOT_TRACK: "1",
283
+ ...(chrome ? { PRODUCER_HEADLESS_SHELL_PATH: chrome, HYPERFRAMES_BROWSER_PATH: chrome } : {}),
284
+ } as NodeJS.ProcessEnv;
285
+ const child = spawn(process.execPath, [FRAMEY_ENTRY, "preview", dir, "--port", String(innerPort), "--no-open", "--foreground", "--force-new"], { env: childEnv, cwd: dir, stdio: ["ignore", "pipe", "pipe"] });
286
+ let output = "";
287
+ const collect = (chunk: Buffer) => {
288
+ output = (output + chunk.toString()).slice(-4000);
289
+ };
290
+ child.stdout?.on("data", collect);
291
+ child.stderr?.on("data", collect);
292
+ // If vfrey itself ends without closing the studio, the editor process must not outlive it.
293
+ const killOnExit = () => child.kill("SIGKILL");
294
+ process.once("exit", killOnExit);
295
+ const stop = async () => {
296
+ process.off("exit", killOnExit);
297
+ if (child.exitCode !== null) return;
298
+ child.kill("SIGTERM");
299
+ await Promise.race([new Promise((resolve) => child.once("exit", resolve)), sleep(4000)]);
300
+ if (child.exitCode === null) child.kill("SIGKILL");
301
+ };
302
+ try {
303
+ await waitUntilUp(innerPort, child, () => output, input.timeoutMs ?? 40_000);
304
+ } catch (error) {
305
+ await stop();
306
+ throw new SceneError(`The studio did not start: ${error instanceof Error ? error.message : String(error)}.`, "run `npx vfrey doctor`, then try again.", "studio_start_failed");
307
+ }
308
+ const front = frontServer(port, innerPort);
309
+ try {
310
+ await new Promise<void>((resolve, reject) => {
311
+ front.once("error", reject);
312
+ front.listen(port, "127.0.0.1", () => resolve());
313
+ });
314
+ } catch (error) {
315
+ await stop();
316
+ throw new SceneError(`The studio could not listen on port ${port}: ${error instanceof Error ? error.message : String(error)}.`, "pass another --port N.", "studio_port_busy");
317
+ }
318
+ const id = path.basename(dir);
319
+ return {
320
+ url: `http://127.0.0.1:${port}/#project/${encodeURIComponent(id)}`,
321
+ port,
322
+ dir,
323
+ close: async () => {
324
+ front.closeAllConnections();
325
+ await new Promise<void>((resolve) => front.close(() => resolve()));
326
+ await stop();
327
+ },
328
+ };
329
+ }
330
+
331
+ /** The edit copy of a run's stored scene: `<scenes>/<name>-edit`. An earlier copy, which holds earlier edits, is kept. */
332
+ export async function prepareEditCopy(input: { storedDir: string; scenesRoot: string; name: string }): Promise<{ dir: string; copied: boolean; warnings: string[] }> {
333
+ const dir = path.join(input.scenesRoot, `${input.name}-edit`);
334
+ if (existsSync(path.join(dir, "index.html"))) return { dir, copied: false, warnings: [] };
335
+ const stored = await storeScene(input.storedDir, dir);
336
+ return { dir, copied: true, warnings: stored.warnings };
337
+ }
@@ -0,0 +1,111 @@
1
+ import { execFile } from "node:child_process";
2
+ import { copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { promisify } from "node:util";
6
+ import { glyphSvg, GLYPH_NAMES } from "./glyphs.js";
7
+ import { figureSvg, POSES } from "./figure.js";
8
+ import type { SceneTiming } from "./timing.js";
9
+
10
+ type Env = Record<string, string | undefined>;
11
+
12
+ const here = path.dirname(fileURLToPath(import.meta.url));
13
+ const assets = path.join(here, "assets");
14
+ /** Framey's shader-transition runtime, synced by scripts/sync-framey-transitions.mjs. */
15
+ export const TRANSITIONS_DIR = path.join(here, "transitions");
16
+ const deskFonts = path.resolve(here, "../desk/fonts");
17
+ const run = promisify(execFile);
18
+
19
+ /** The folder inside a scene that vfrey writes at render time. Anything the scene put there is replaced. */
20
+ export const SUPPLY_DIR = "vfrey";
21
+
22
+ /** The font files vfrey supplies, and the family each one is. */
23
+ export const SUPPLIED_FONTS = [
24
+ { family: "Titan One", file: "titan-one.woff2", from: path.join(assets, "fonts", "titan-one.woff2"), weight: 400 },
25
+ { family: "JetBrains Mono", file: "jetbrains-mono-medium.woff2", from: path.join(deskFonts, "jetbrains-mono-medium.woff2"), weight: 500 },
26
+ { family: "Google Sans Flex", file: "google-sans-flex-latin.woff2", from: path.join(deskFonts, "google-sans-flex-latin.woff2"), weight: 400 },
27
+ ] as const;
28
+
29
+ const FONT_LICENSES = [
30
+ { file: "TitanOne-OFL.txt", from: path.join(assets, "fonts", "TitanOne-OFL.txt") },
31
+ { file: "JetBrainsMono-OFL.txt", from: path.join(deskFonts, "JetBrainsMono-OFL.txt") },
32
+ ];
33
+
34
+ let helperCache: string | null = null;
35
+
36
+ /** The browser helper, bundled from `runtime/browser.ts` to one classic script. */
37
+ export async function helperSource(): Promise<string> {
38
+ if (helperCache) return helperCache;
39
+ const { build } = await import("esbuild");
40
+ const result = await build({
41
+ entryPoints: [path.join(here, "runtime", "browser.ts")],
42
+ bundle: true,
43
+ format: "iife",
44
+ platform: "browser",
45
+ target: "es2020",
46
+ write: false,
47
+ logLevel: "silent",
48
+ });
49
+ helperCache = result.outputFiles[0]!.text;
50
+ return helperCache;
51
+ }
52
+
53
+ export function fontsCss(): string {
54
+ return `${SUPPLIED_FONTS.map(
55
+ (f) => `@font-face { font-family: "${f.family}"; src: url("${f.file}") format("woff2"); font-weight: ${f.weight}; font-style: normal; }`,
56
+ ).join("\n")}\n`;
57
+ }
58
+
59
+ /** The voice as `voice.wav`: copied when it already is one, converted with ffmpeg otherwise. */
60
+ export async function writeVoice(voiceFile: string, dest: string, env: Env): Promise<void> {
61
+ if (voiceFile.toLowerCase().endsWith(".wav")) {
62
+ await copyFile(voiceFile, dest);
63
+ return;
64
+ }
65
+ await run("ffmpeg", ["-y", "-v", "error", "-i", voiceFile, "-ar", "44100", "-ac", "1", dest], { env: env as NodeJS.ProcessEnv, timeout: 120_000 });
66
+ }
67
+
68
+ export interface SupplyInput {
69
+ /** The scene folder; `vfrey/` is written inside it. */
70
+ dir: string;
71
+ timing: SceneTiming;
72
+ /** The voice track, or null for a silent render or a preview. */
73
+ voiceFile: string | null;
74
+ /** `html` scenes get the helper and GSAP; `tsx` scenes get `@vfrey/scene` from the bundler instead. */
75
+ kind: "html" | "tsx";
76
+ env?: Env;
77
+ }
78
+
79
+ /** Writes everything vfrey supplies to a scene into `dir/vfrey/`. */
80
+ export async function writeSupply(input: SupplyInput): Promise<void> {
81
+ const out = path.join(input.dir, SUPPLY_DIR);
82
+ await rm(out, { recursive: true, force: true });
83
+ await mkdir(path.join(out, "fonts"), { recursive: true });
84
+ await mkdir(path.join(out, "assets"), { recursive: true });
85
+ await writeFile(path.join(out, "timing.json"), `${JSON.stringify(input.timing, null, 2)}\n`);
86
+ if (input.voiceFile) await writeVoice(input.voiceFile, path.join(out, "voice.wav"), input.env ?? process.env);
87
+ for (const font of SUPPLIED_FONTS) await copyFile(font.from, path.join(out, "fonts", font.file));
88
+ for (const license of FONT_LICENSES) await copyFile(license.from, path.join(out, "fonts", license.file));
89
+ await writeFile(path.join(out, "fonts", "fonts.css"), fontsCss());
90
+ if (input.kind === "html") {
91
+ await copyFile(path.join(assets, "gsap", "gsap.min.js"), path.join(out, "gsap.min.js"));
92
+ await copyFile(path.join(assets, "gsap", "LICENSE.txt"), path.join(out, "GSAP-LICENSE.txt"));
93
+ await copyFile(path.join(TRANSITIONS_DIR, "transitions.global.js"), path.join(out, "transitions.js"));
94
+ await writeFile(path.join(out, "TRANSITIONS-LICENSE.txt"), `${await readFile(path.join(TRANSITIONS_DIR, "NOTICE"), "utf8")}\n${await readFile(path.join(TRANSITIONS_DIR, "LICENSE"), "utf8")}`);
95
+ await writeFile(path.join(out, "vfrey.js"), `window.__vfreyTiming = ${JSON.stringify(input.timing)};\n${await helperSource()}`);
96
+ }
97
+ for (const [file, svg] of Object.entries(assetLibrary())) await writeFile(path.join(out, "assets", file), svg);
98
+ }
99
+
100
+ /** The asset library: stick-figure poses and line glyphs, as `file name -> SVG`. */
101
+ export function assetLibrary(): Record<string, string> {
102
+ const files: Record<string, string> = {};
103
+ for (const pose of POSES) files[`stick-${pose.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}.svg`] = figureSvg(pose);
104
+ for (const name of GLYPH_NAMES) files[`glyph-${name}.svg`] = glyphSvg(name);
105
+ return files;
106
+ }
107
+
108
+ /** Reads a supplied file back, for tests and the starter. */
109
+ export async function readSupplied(dir: string, name: string): Promise<string> {
110
+ return readFile(path.join(dir, SUPPLY_DIR, name), "utf8");
111
+ }