@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,67 @@
1
+ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ export const KEY_PLATFORMS = ["youtube", "tiktok", "instagram", "facebook"] as const;
5
+ export type KeyPlatform = (typeof KEY_PLATFORMS)[number];
6
+
7
+ export type KeyFile = Partial<Record<KeyPlatform, string>>;
8
+
9
+ export function parseKeyFile(raw: string): KeyFile {
10
+ if (!raw.trim()) return {};
11
+ let parsed: unknown;
12
+ try {
13
+ parsed = JSON.parse(raw);
14
+ } catch {
15
+ throw new Error("Key file is not JSON.");
16
+ }
17
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
18
+ throw new Error("Key file must be an object.");
19
+ }
20
+ const out: KeyFile = {};
21
+ for (const platform of KEY_PLATFORMS) {
22
+ const value = (parsed as Record<string, unknown>)[platform];
23
+ if (typeof value === "string" && value.trim()) out[platform] = value;
24
+ }
25
+ return out;
26
+ }
27
+
28
+ export function keyPresence(keys: KeyFile): Record<KeyPlatform, boolean> {
29
+ return {
30
+ youtube: Boolean(keys.youtube),
31
+ tiktok: Boolean(keys.tiktok),
32
+ instagram: Boolean(keys.instagram),
33
+ facebook: Boolean(keys.facebook),
34
+ };
35
+ }
36
+
37
+ export function hasPlatformKey(keys: KeyFile, platform: string): boolean {
38
+ if (!KEY_PLATFORMS.includes(platform as KeyPlatform)) return false;
39
+ return Boolean(keys[platform as KeyPlatform]);
40
+ }
41
+
42
+ export function withKey(keys: KeyFile, platform: string, value: string): KeyFile {
43
+ if (!KEY_PLATFORMS.includes(platform as KeyPlatform)) {
44
+ throw new Error(`Unknown platform ${platform}.`);
45
+ }
46
+ const next: KeyFile = { ...keys };
47
+ const trimmed = value.trim();
48
+ if (!trimmed) delete next[platform as KeyPlatform];
49
+ else next[platform as KeyPlatform] = trimmed;
50
+ return next;
51
+ }
52
+
53
+ export async function readKeys(filePath: string): Promise<KeyFile> {
54
+ try {
55
+ return parseKeyFile(await readFile(filePath, "utf8"));
56
+ } catch (error) {
57
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return {};
58
+ throw error;
59
+ }
60
+ }
61
+
62
+ export async function writeKeys(filePath: string, keys: KeyFile): Promise<void> {
63
+ await mkdir(path.dirname(filePath), { recursive: true });
64
+ const body = `${JSON.stringify(keys)}\n`;
65
+ await writeFile(filePath, body, { mode: 0o600 });
66
+ await chmod(filePath, 0o600);
67
+ }
@@ -0,0 +1,18 @@
1
+ import path from "node:path";
2
+ import { dataDirFor } from "../home.js";
3
+
4
+ export function productDir(env: NodeJS.ProcessEnv): string {
5
+ return env.PRODUCT_HOME ? path.resolve(env.PRODUCT_HOME) : path.join(dataDirFor(env), "product");
6
+ }
7
+
8
+ export function keyFile(dir: string): string {
9
+ return path.join(dir, "keys.json");
10
+ }
11
+
12
+ export function insightFile(dir: string): string {
13
+ return path.join(dir, "insights.json");
14
+ }
15
+
16
+ export function configFile(dir: string): string {
17
+ return path.join(dir, "config.json");
18
+ }
@@ -0,0 +1,88 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { constants, existsSync, accessSync } from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import path from "node:path";
5
+ import { headlessShellMissing, onPath, PACKAGE_ROOT, resolveHeadlessShell } from "../render/chrome.js";
6
+ import type { EngineProbes } from "./engines.js";
7
+
8
+ type Env = Record<string, string | undefined>;
9
+
10
+ export const FRAMEY_ENTRY = path.join(PACKAGE_ROOT, "vendor/framey/bin/framey.mjs");
11
+
12
+ /** Why the vendored Framey renderer cannot run here, or null when it can. */
13
+ export function frameyRendererProblem(): string | null {
14
+ if (!existsSync(FRAMEY_ENTRY)) return "the Framey renderer is missing from this install; reinstall vfrey";
15
+ try {
16
+ createRequire(FRAMEY_ENTRY).resolve("puppeteer-core");
17
+ } catch {
18
+ return "the Framey renderer's dependencies are not installed; run npm install in the vfrey folder";
19
+ }
20
+ return null;
21
+ }
22
+
23
+ /** Why an HTML engine has no Chrome or ffmpeg here, or null when it has both. */
24
+ export function renderEnvProblem(env: Env, cwd: string): string | null {
25
+ if (!onPath("ffmpeg", env)) return "ffmpeg is not on PATH; install ffmpeg";
26
+ if (!resolveHeadlessShell(env, cwd)) {
27
+ return "Chrome Headless Shell is not downloaded yet; run `npx remotion browser ensure`";
28
+ }
29
+ return null;
30
+ }
31
+
32
+ function isExecutable(file: string): boolean {
33
+ try {
34
+ accessSync(file, constants.X_OK);
35
+ return true;
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+
41
+ /** The upstream HyperFrames CLI to run: HYPERFRAMES_BIN, a local install, or one on PATH. */
42
+ export function hyperframesCandidates(env: Env, cwd: string): string[] {
43
+ const found: string[] = [];
44
+ if (env.HYPERFRAMES_BIN) found.push(env.HYPERFRAMES_BIN);
45
+ for (const root of [cwd, PACKAGE_ROOT]) found.push(path.join(root, "node_modules/.bin/hyperframes"));
46
+ for (const dir of (env.PATH ?? "").split(path.delimiter)) if (dir) found.push(path.join(dir, "hyperframes"));
47
+ return found.filter((file) => isExecutable(file));
48
+ }
49
+
50
+ /** The command line that runs a HyperFrames entry: a script goes through node, a binary runs as is. */
51
+ export function commandFor(bin: string): { command: string; args: string[] } {
52
+ return /\.(mjs|cjs|js)$/.test(bin) ? { command: process.execPath, args: [bin] } : { command: bin, args: [] };
53
+ }
54
+
55
+ /** The first HyperFrames CLI that answers `--version`, with the version line. */
56
+ export function hyperframesCli(env: Env, cwd: string): { bin: string; version: string } | null {
57
+ for (const bin of hyperframesCandidates(env, cwd)) {
58
+ const { command, args } = commandFor(bin);
59
+ try {
60
+ const out = execFileSync(command, [...args, "--version"], { env: env as NodeJS.ProcessEnv, timeout: 8000, stdio: ["ignore", "pipe", "ignore"] });
61
+ return { bin, version: out.toString().trim().split(/\r?\n/)[0] ?? "" };
62
+ } catch {
63
+ /* not a working CLI; try the next one */
64
+ }
65
+ }
66
+ return null;
67
+ }
68
+
69
+ /** Why Framey has no ffmpeg here, or null. A missing Chrome Headless Shell is not a problem: the first render downloads it. */
70
+ function frameyEnvProblem(env: Env): string | null {
71
+ return onPath("ffmpeg", env) ? null : "ffmpeg is not on PATH; install ffmpeg";
72
+ }
73
+
74
+ /** Everything the engine list needs, probed without a network call. */
75
+ export function probeEnginesSync(env: Env, cwd: string = process.cwd()): EngineProbes {
76
+ return {
77
+ hyperframesCli: hyperframesCli(env, cwd) !== null,
78
+ comfyReachable: false,
79
+ frameyProblem: frameyRendererProblem() ?? frameyEnvProblem(env),
80
+ frameyDownloadsChrome: headlessShellMissing(env, cwd),
81
+ renderEnvProblem: renderEnvProblem(env, cwd),
82
+ };
83
+ }
84
+
85
+ /** Probes for the dashboard and the render stage. ComfyUI is not wired, so no server is asked. */
86
+ export async function defaultProbes(env: Env, cwd: string = process.cwd()): Promise<EngineProbes> {
87
+ return probeEnginesSync(env, cwd);
88
+ }
@@ -0,0 +1,90 @@
1
+ export type DescriptionChoice = "type" | "auto";
2
+ export type ThumbnailChoice = "same" | "address" | "generate";
3
+
4
+ export interface PushInput {
5
+ platform: string;
6
+ hasKey: boolean;
7
+ description?: DescriptionChoice | null;
8
+ descriptionText?: string | null;
9
+ thumbnail?: ThumbnailChoice | null;
10
+ thumbnailAddress?: string | null;
11
+ }
12
+
13
+ export type PushPhase = "stopped" | "ask" | "ready";
14
+
15
+ export interface PushDecision {
16
+ phase: PushPhase;
17
+ message: string;
18
+ uploadAttempted: false;
19
+ question?: "description" | "description-text" | "thumbnail" | "thumbnail-address";
20
+ }
21
+
22
+ const DESCRIPTION = new Set<DescriptionChoice>(["type", "auto"]);
23
+ const THUMBNAIL = new Set<ThumbnailChoice>(["same", "address", "generate"]);
24
+
25
+ export function decidePush(input: PushInput): PushDecision {
26
+ const platform = input.platform.trim().toLowerCase() || "youtube";
27
+ const name = platform === "youtube" ? "YouTube" : platform;
28
+ if (!input.hasKey) {
29
+ return {
30
+ phase: "stopped",
31
+ uploadAttempted: false,
32
+ message: `The ${name} key is not configured. This video cannot be pushed there.`,
33
+ };
34
+ }
35
+ if (!input.description || !DESCRIPTION.has(input.description)) {
36
+ return {
37
+ phase: "ask",
38
+ uploadAttempted: false,
39
+ question: "description",
40
+ message: "Description: type one, or auto-generate?",
41
+ };
42
+ }
43
+ if (input.description === "type" && !input.descriptionText?.trim()) {
44
+ return {
45
+ phase: "ask",
46
+ uploadAttempted: false,
47
+ question: "description-text",
48
+ message: "Type the description to use on the post.",
49
+ };
50
+ }
51
+ if (!input.thumbnail || !THUMBNAIL.has(input.thumbnail)) {
52
+ return {
53
+ phase: "ask",
54
+ uploadAttempted: false,
55
+ question: "thumbnail",
56
+ message: "Thumbnail: use the same thumbnail, give an address, or generate one?",
57
+ };
58
+ }
59
+ if (input.thumbnail === "address" && !input.thumbnailAddress?.trim()) {
60
+ return {
61
+ phase: "ask",
62
+ uploadAttempted: false,
63
+ question: "thumbnail-address",
64
+ message: "Give the address of the thumbnail.",
65
+ };
66
+ }
67
+ const description =
68
+ input.description === "auto" ? "an auto-generated description" : "the description you typed";
69
+ const thumbnail =
70
+ input.thumbnail === "same"
71
+ ? "the same thumbnail"
72
+ : input.thumbnail === "generate"
73
+ ? "a generated thumbnail"
74
+ : "the thumbnail at the address you gave";
75
+ return {
76
+ phase: "ready",
77
+ uploadAttempted: false,
78
+ message: `Ready to publish to ${name} with ${description} and ${thumbnail}.`,
79
+ };
80
+ }
81
+
82
+ export async function runPush(
83
+ input: PushInput,
84
+ upload: () => Promise<void>,
85
+ ): Promise<PushDecision & { uploaded: boolean }> {
86
+ const decision = decidePush(input);
87
+ if (decision.phase !== "ready") return { ...decision, uploaded: false };
88
+ await upload();
89
+ return { ...decision, uploaded: true };
90
+ }
@@ -0,0 +1,21 @@
1
+ import { stat } from "node:fs/promises";
2
+ import type { RenderProps } from "@vfrey/contracts";
3
+ import { assertCanRender, type EngineAvailability, type EngineId, type EngineSpec } from "./engines.js";
4
+
5
+ export interface RenderRequest {
6
+ engine: EngineSpec;
7
+ availability: EngineAvailability;
8
+ /** Why the engine is unavailable, for the refusal. */
9
+ reason?: string | null;
10
+ outputPath: string;
11
+ props: RenderProps;
12
+ renderLocal: (outputPath: string, props: RenderProps, engine: EngineId) => Promise<void>;
13
+ }
14
+
15
+ export async function renderChosenEngine(request: RenderRequest): Promise<{ outputPath: string; bytes: number }> {
16
+ assertCanRender(request.engine, request.availability, request.reason);
17
+ await request.renderLocal(request.outputPath, request.props, request.engine.id);
18
+ const info = await stat(request.outputPath);
19
+ if (info.size <= 0) throw new Error("Render wrote an empty file.");
20
+ return { outputPath: request.outputPath, bytes: info.size };
21
+ }
@@ -0,0 +1,62 @@
1
+ import { TEMPLATES } from "@vfrey/contracts";
2
+ import type { Beat, RenderProps, TemplateId } from "@vfrey/contracts";
3
+
4
+ export const SHORT_FRAME = { width: 1080, height: 1920, fps: 30 } as const;
5
+
6
+ const ROLES = ["hook", "setup", "turn", "evidence", "cta"] as const;
7
+ const WEIGHTS = [0.18, 0.22, 0.22, 0.2, 0.18];
8
+
9
+ export function parseComposition(value: string | null | undefined): TemplateId {
10
+ if (!value?.trim()) return "KineticType";
11
+ const found = (Object.keys(TEMPLATES) as TemplateId[]).find((id) => id.toLowerCase() === value.trim().toLowerCase());
12
+ if (!found) throw new Error("Unknown composition. Choose KineticType, SignalBoard, or EditorialSplit.");
13
+ return found;
14
+ }
15
+
16
+ function round3(value: number): number {
17
+ return Math.round(value * 1000) / 1000;
18
+ }
19
+
20
+ export function durationInBand(composition: TemplateId, seconds: number): boolean {
21
+ const spec = TEMPLATES[composition];
22
+ return seconds >= spec.durationMinSec && seconds <= spec.durationMaxSec;
23
+ }
24
+
25
+ /** On-screen package for a vertical short. The hook caption is the subject. */
26
+ export function buildShort(subject: string, composition: TemplateId = "KineticType"): RenderProps {
27
+ const trimmed = subject.trim();
28
+ if (!trimmed) throw new Error("A subject is required.");
29
+ const spec = TEMPLATES[composition];
30
+ let used = 0;
31
+ const captions = [
32
+ trimmed,
33
+ `What ${trimmed} does`,
34
+ `How to use ${trimmed}`,
35
+ `Why ${trimmed} is useful`,
36
+ `Start with ${trimmed}`,
37
+ ];
38
+ const beats: Beat[] = WEIGHTS.map((weight, index) => {
39
+ const seconds =
40
+ index === WEIGHTS.length - 1 ? round3(spec.durationTargetSec - used) : round3(spec.durationTargetSec * weight);
41
+ used = round3(used + seconds);
42
+ return { role: ROLES[index]!, text: captions[index]!, caption: captions[index]!, seconds };
43
+ });
44
+ const durationSec = round3(beats.reduce((sum, beat) => sum + beat.seconds, 0));
45
+ if (!durationInBand(composition, durationSec)) {
46
+ throw new Error(`${composition} duration ${durationSec}s is outside its published band.`);
47
+ }
48
+ const palette = spec.palettes[0];
49
+ if (!palette) throw new Error(`${composition} has no palette.`);
50
+ return {
51
+ templateId: composition,
52
+ title: trimmed,
53
+ hook: trimmed,
54
+ beats,
55
+ palette,
56
+ captionStyle: spec.captionStyle,
57
+ pacing: spec.pacing,
58
+ voiceoverFile: null,
59
+ layoutSeed: 2,
60
+ durationSec,
61
+ };
62
+ }
@@ -0,0 +1,32 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { packageVersion } from "../skill-install.js";
4
+
5
+ /** The folder holding this package's package.json, wherever the caller's shell is. */
6
+ export function updateInstallRoot(): string {
7
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
8
+ // Installed from npm, this package sits at <project>/node_modules/@vfrey/vfrey; the update runs in <project>.
9
+ const marker = `${path.sep}node_modules${path.sep}`;
10
+ const at = root.lastIndexOf(marker);
11
+ return at === -1 ? root : root.slice(0, at);
12
+ }
13
+
14
+ /** Commands the owner runs to refresh the app and the installed skills. The agent-neutral skill refresh is `vfrey skill install --agent <agent>`. */
15
+ export function updateCommands(version: string = packageVersion()): readonly string[] {
16
+ // A prerelease (1.0.0-beta.0) is matched only by exact pins, so a beta follows the beta tag.
17
+ const prerelease = /^\d+\.\d+\.\d+-/.test(version);
18
+ return [
19
+ "claude plugin update vfrey@vfrey",
20
+ prerelease ? "npm install @vfrey/vfrey@beta" : "npm install",
21
+ ];
22
+ }
23
+
24
+ export function formatUpdatePlan(apply: boolean, version: string = packageVersion()): string {
25
+ const lines = [
26
+ "Update the vfrey app and the skills:",
27
+ ...updateCommands(version).map((command) => `- ${command}`),
28
+ "Not on Claude Code? Refresh the skill for your agent instead: npx @vfrey/vfrey skill install --agent codex|grok|cursor|gemini (or all).",
29
+ ];
30
+ if (!apply) lines.push("Printed only. Pass --apply to run them.");
31
+ return lines.join("\n");
32
+ }
@@ -0,0 +1,14 @@
1
+ import { realpathSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ /** The real path of `p` (symlinks resolved), or its absolute form when it does not exist yet. Compare paths only in this form. */
5
+ export function canonicalPath(p: string): string {
6
+ const abs = path.resolve(p);
7
+ try {
8
+ return realpathSync.native(abs);
9
+ } catch {
10
+ // Not there yet: canonicalize the nearest existing parent so a new folder behind a symlink still compares equal.
11
+ const parent = path.dirname(abs);
12
+ return parent === abs ? abs : path.join(canonicalPath(parent), path.basename(abs));
13
+ }
14
+ }
@@ -0,0 +1,177 @@
1
+ import { existsSync } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { DatabaseSync } from "node:sqlite";
5
+ import { resolveHome } from "../home.js";
6
+ import { ENGINE_IDS } from "../product/engines.js";
7
+ import { Store } from "../queue/store.js";
8
+ import { dashboardStatus } from "./dashboard-status.js";
9
+ import { isMissing, pruneIndex, readIndex, registerProject, touchProject } from "./index-file.js";
10
+ import { canonicalPath } from "./canonical.js";
11
+ import { ProjectError } from "./marker.js";
12
+ import { importDesk } from "./migrate.js";
13
+ import { dayStamp } from "./names.js";
14
+ import { projectJson, projectNotice } from "./notice.js";
15
+ import { scaffoldProject } from "./scaffold.js";
16
+ import { liveHolder } from "../service/lock.js";
17
+ import { readState } from "../service/state.js";
18
+ import { listVideos } from "./videos.js";
19
+
20
+ export interface ProjectIo {
21
+ env: Record<string, string | undefined>;
22
+ cwd: string;
23
+ homedir?: string;
24
+ stdout: (text: string) => void;
25
+ stderr: (text: string) => void;
26
+ }
27
+
28
+ function flag(argv: string[], name: string): string | null {
29
+ const at = argv.indexOf(name);
30
+ if (at < 0) return null;
31
+ const value = argv[at + 1];
32
+ return value === undefined || value.startsWith("--") ? "" : value;
33
+ }
34
+
35
+ const fail = (io: ProjectIo, message: string, fix: string): number => {
36
+ io.stderr(`✗ ${message}\nFix: ${fix}\n`);
37
+ return 1;
38
+ };
39
+
40
+ function length(seconds: number | null): string {
41
+ if (seconds === null) return "-";
42
+ const whole = Math.round(seconds);
43
+ return `${Math.floor(whole / 60)}:${String(whole % 60).padStart(2, "0")}`;
44
+ }
45
+
46
+ /** `vfrey init [--name NAME] [--engine ENGINE] [--from DIR]`. */
47
+ export function runInit(argv: string[], io: ProjectIo): number {
48
+ const from = flag(argv, "--from");
49
+ if (from === "") return fail(io, "--from needs a folder.", "for example `npx vfrey init --from ~/.vfrey`.");
50
+ const name = flag(argv, "--name");
51
+ if (name === "") return fail(io, "--name needs a name.", "for example `npx vfrey init --name my-channel`.");
52
+ const engine = flag(argv, "--engine");
53
+ if (engine !== null && !(ENGINE_IDS as readonly string[]).includes(engine)) {
54
+ return fail(io, `"${engine}" is not an engine.`, `choose ${ENGINE_IDS.join(", ")}.`);
55
+ }
56
+ const home = resolveHome(io.env, io.homedir ?? os.homedir(), io.cwd);
57
+ try {
58
+ const result = scaffoldProject(canonicalPath(io.cwd), { name: name ?? undefined, engine: engine ?? undefined });
59
+ registerProject(home.machineHome, result.project);
60
+ const what = result.existing ? `Repaired the vfrey project ${result.project.name}` : `Started the vfrey project ${result.project.name}`;
61
+ io.stdout(`${what} in ${result.project.root}\n`);
62
+ for (const item of result.created) io.stdout(` ${result.existing ? "repaired" : "+"} ${item}\n`);
63
+ for (const item of result.kept) io.stdout(` kept ${item}\n`);
64
+ if (from !== null) {
65
+ const counts = importDesk(from, result.project);
66
+ io.stdout(
67
+ `Imported ${path.resolve(from)}: ${counts.jobs} runs (${counts.otherRows} other rows), ${counts.videos} videos, ${counts.transcripts} transcripts, ${counts.scenes} scenes.\n`,
68
+ );
69
+ if (counts.alreadyImported > 0) io.stdout(` ${counts.alreadyImported} videos were already imported and were left alone.\n`);
70
+ if (counts.withoutVideo > 0) io.stdout(` ${counts.withoutVideo} runs have no video file (failed, pending or deleted); their rows were copied.\n`);
71
+ io.stdout("The source was not changed.\n");
72
+ }
73
+ io.stdout("Videos land in videos/. Make one: npx vfrey tick --now --about \"<subject>\"\n");
74
+ return 0;
75
+ } catch (error) {
76
+ if (error instanceof ProjectError) return fail(io, error.reason, error.fix);
77
+ throw error;
78
+ }
79
+ }
80
+
81
+ /** `vfrey videos [--json] [--limit N]`, newest first. */
82
+ export function runVideos(argv: string[], io: ProjectIo): number {
83
+ const limitRaw = flag(argv, "--limit");
84
+ let limit: number | undefined;
85
+ if (limitRaw !== null) {
86
+ limit = Number(limitRaw);
87
+ if (!Number.isInteger(limit) || limit < 1) return fail(io, `--limit needs a whole number of 1 or more, not "${limitRaw}".`, "for example `npx vfrey videos --limit 5`.");
88
+ }
89
+ const home = resolveHome(io.env, io.homedir ?? os.homedir(), io.cwd);
90
+ const notice = projectNotice(home);
91
+ if (notice) io.stderr(notice);
92
+ if (home.project) touchProject(home.machineHome, home.project);
93
+ const store = new Store(path.join(home.dataDir, "pipeline.sqlite"));
94
+ let videos;
95
+ try {
96
+ videos = listVideos(store.db, home.dataDir, limit);
97
+ } finally {
98
+ store.close();
99
+ }
100
+ if (argv.includes("--json")) {
101
+ io.stdout(
102
+ JSON.stringify(
103
+ { project: projectJson(home), dataDir: home.dataDir, videos: videos.map((v) => ({ id: v.id, date: v.date, title: v.title, durationSec: v.durationSec, path: v.path })) },
104
+ null,
105
+ 2,
106
+ ) + "\n",
107
+ );
108
+ return 0;
109
+ }
110
+ if (videos.length === 0) {
111
+ io.stdout("No videos yet. Make one: npx vfrey tick --now --about \"<subject>\"\n");
112
+ return 0;
113
+ }
114
+ for (const v of videos) io.stdout(`${dayStamp(new Date(v.date))} ${length(v.durationSec).padStart(5)} ${v.title ?? "(untitled)"} ${v.path}\n`);
115
+ return 0;
116
+ }
117
+
118
+ function lastVideoOf(root: string): { title: string | null; date: string; path: string } | null {
119
+ const dataDir = path.join(root, ".vfrey");
120
+ const file = path.join(dataDir, "pipeline.sqlite");
121
+ if (!existsSync(file)) return null;
122
+ let db: DatabaseSync | null = null;
123
+ try {
124
+ db = new DatabaseSync(file, { readOnly: true });
125
+ const [first] = listVideos(db, dataDir, 1);
126
+ return first ? { title: first.title, date: first.date, path: first.path } : null;
127
+ } catch {
128
+ return null;
129
+ } finally {
130
+ db?.close();
131
+ }
132
+ }
133
+
134
+ /** `vfrey projects [--prune] [--json]`: the machine index. */
135
+ export async function runProjects(argv: string[], io: ProjectIo): Promise<number> {
136
+ const home = resolveHome(io.env, io.homedir ?? os.homedir(), io.cwd);
137
+ if (home.project) touchProject(home.machineHome, home.project);
138
+ const pruned = argv.includes("--prune") ? pruneIndex(home.machineHome) : [];
139
+ const entries = readIndex(home.machineHome).sort((a, b) => b.lastUsed.localeCompare(a.lastUsed));
140
+ const statuses = await Promise.all(entries.map((entry) => (isMissing(entry) ? Promise.resolve({ status: "unknown" as const, url: null }) : dashboardStatus(entry.root, io.env))));
141
+ const workerState = readState(home.machineHome);
142
+ const workerOf = (entry: (typeof entries)[number]): { state: string; lastTick: string | null } => {
143
+ const lastTick = workerState.projects[entry.id]?.lastTick ?? null;
144
+ if (isMissing(entry)) return { state: "-", lastTick };
145
+ if (liveHolder(path.join(entry.root, ".vfrey"))) return { state: "ticking", lastTick };
146
+ return { state: lastTick ? `last tick ${dayStamp(new Date(lastTick))}` : "never ticked", lastTick };
147
+ };
148
+ const rows = entries.map((entry, at) => ({
149
+ worker: workerOf(entry),
150
+ id: entry.id,
151
+ name: entry.name,
152
+ root: entry.root,
153
+ lastUsed: entry.lastUsed,
154
+ missing: isMissing(entry),
155
+ lastVideo: isMissing(entry) ? null : lastVideoOf(entry.root),
156
+ dashboard: statuses[at]!.status,
157
+ dashboardUrl: statuses[at]!.url,
158
+ }));
159
+ if (argv.includes("--json")) {
160
+ io.stdout(JSON.stringify({ projects: rows, pruned: pruned.map((p) => ({ id: p.id, name: p.name, root: p.root })) }, null, 2) + "\n");
161
+ return 0;
162
+ }
163
+ for (const p of pruned) io.stdout(`Removed ${p.name} (${p.root}): the folder is gone.\n`);
164
+ if (rows.length === 0) {
165
+ io.stdout("No projects yet. Start one: npm create vfrey@latest my-channel\n");
166
+ return 0;
167
+ }
168
+ for (const r of rows) {
169
+ if (r.missing) {
170
+ io.stdout(`${r.name} ${r.root} missing (run \`npx vfrey projects --prune\` to drop it)\n`);
171
+ continue;
172
+ }
173
+ const last = r.lastVideo ? `last video ${dayStamp(new Date(r.lastVideo.date))} ${r.lastVideo.title ?? "(untitled)"}` : "no videos yet";
174
+ io.stdout(`${r.name} ${r.root} ${last} dashboard ${r.dashboard}${r.dashboardUrl ? ` ${r.dashboardUrl}` : ""} worker ${r.worker.state}\n`);
175
+ }
176
+ return 0;
177
+ }
@@ -0,0 +1,28 @@
1
+ import path from "node:path";
2
+ import { canonicalPath } from "./canonical.js";
3
+ import { findDashboard } from "../dashboard-link.js";
4
+ import { MARKER_FILE, readMarker } from "./marker.js";
5
+ import { existsSync } from "node:fs";
6
+
7
+ /** Whether a project's dashboard is listening: its `.vfrey/dashboard.json` and `GET /api/state` naming this project's id. */
8
+ export type DashboardStatus = "up" | "down" | "unknown";
9
+
10
+ export interface DashboardInfo {
11
+ status: DashboardStatus;
12
+ url: string | null;
13
+ }
14
+
15
+ /** "up" with the URL, "down", or "unknown" when the project's marker cannot be read. */
16
+ export async function dashboardStatus(rootPath: string, env: Record<string, string | undefined> = process.env): Promise<DashboardInfo> {
17
+ const projectRoot = canonicalPath(rootPath);
18
+ if (!existsSync(path.join(projectRoot, MARKER_FILE))) return { status: "unknown", url: null };
19
+ let id: string;
20
+ let name: string;
21
+ try {
22
+ ({ id, name } = readMarker(projectRoot));
23
+ } catch {
24
+ return { status: "unknown", url: null };
25
+ }
26
+ const origin = await findDashboard({ dataDir: path.join(projectRoot, ".vfrey"), project: { id, name } }, env);
27
+ return origin ? { status: "up", url: origin } : { status: "down", url: null };
28
+ }
@@ -0,0 +1,19 @@
1
+ import path from "node:path";
2
+ import { canonicalPath } from "./canonical.js";
3
+ import { hasMarker, projectAt, type Project } from "./marker.js";
4
+
5
+ /** The nearest folder at or above `cwd` that holds `vfrey.json`, the way git finds `.git`. */
6
+ export function findProjectRoot(cwd: string): string | null {
7
+ let dir = canonicalPath(cwd);
8
+ for (;;) {
9
+ if (hasMarker(dir)) return dir;
10
+ const parent = path.dirname(dir);
11
+ if (parent === dir) return null;
12
+ dir = parent;
13
+ }
14
+ }
15
+
16
+ export function findProject(cwd: string): Project | null {
17
+ const root = findProjectRoot(cwd);
18
+ return root ? projectAt(root) : null;
19
+ }