@vfrey/vfrey 0.0.1 → 1.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +33 -0
- package/README.md +78 -3
- package/accounts.example.json +61 -0
- package/accounts.first-run.json +41 -0
- package/bin/vfrey.mjs +36 -0
- package/integration-tools/README.md +43 -0
- package/integration-tools/image/pillow.md +32 -0
- package/integration-tools/image/playwright.md +33 -0
- package/integration-tools/image/resvg.md +36 -0
- package/integration-tools/image/satori.md +36 -0
- package/integration-tools/image/sharp.md +38 -0
- package/integration-tools/video/blender.md +34 -0
- package/integration-tools/video/ffmpeg.md +41 -0
- package/integration-tools/video/manim.md +37 -0
- package/integration-tools/video/motion-canvas.md +37 -0
- package/integration-tools/video/remotion.md +50 -0
- package/integration-tools/video/revideo.md +32 -0
- package/package.json +74 -6
- package/skills/vfrey-orchestrator/SKILL.md +15 -0
- package/skills/vfrey-orchestrator/channel-video/SKILL.md +41 -0
- package/skills/vfrey-product/SKILL.md +63 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/cook-one.svg +14 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/cook-two.svg +22 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/index.html +70 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/cook-one.svg +14 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/cook-two.svg +22 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/index.tsx +49 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/script.json +15 -0
- package/skills/vfrey-product/examples/octopus-facts/index.html +60 -0
- package/skills/vfrey-product/examples/octopus-facts/octopus.svg +8 -0
- package/skills/vfrey-product/examples/octopus-facts/script.json +14 -0
- package/skills/vfrey-product/push/SKILL.md +15 -0
- package/skills/vfrey-product/scenes/NOTICE +5 -0
- package/skills/vfrey-product/scenes/blocks.md +57 -0
- package/skills/vfrey-product/scenes/craft.md +118 -0
- package/skills/vfrey-product/scenes/motion.md +88 -0
- package/src/analytics/dashboard.ts +109 -0
- package/src/analytics/index.ts +260 -0
- package/src/api/client.ts +282 -0
- package/src/api/commands.ts +330 -0
- package/src/api/config.ts +34 -0
- package/src/api/creators.ts +463 -0
- package/src/api/credentials.ts +152 -0
- package/src/api/mail.ts +333 -0
- package/src/api/topics.ts +207 -0
- package/src/api/types.ts +205 -0
- package/src/cli.ts +467 -0
- package/src/cloud/executor.ts +281 -0
- package/src/cloud/job.ts +106 -0
- package/src/cloud/render-job.ts +187 -0
- package/src/config.ts +309 -0
- package/src/dashboard-link.ts +180 -0
- package/src/dashboard-start.ts +103 -0
- package/src/desk/channel.ts +64 -0
- package/src/desk/cli.ts +216 -0
- package/src/desk/creators-route.ts +129 -0
- package/src/desk/fonts/JetBrainsMono-OFL.txt +93 -0
- package/src/desk/fonts/google-sans-flex-latin.woff2 +0 -0
- package/src/desk/fonts/jetbrains-mono-medium.woff2 +0 -0
- package/src/desk/http.ts +460 -0
- package/src/desk/mail-route.ts +103 -0
- package/src/desk/overlay.ts +112 -0
- package/src/desk/preview.ts +422 -0
- package/src/desk/screens.ts +149 -0
- package/src/desk/secrets.ts +143 -0
- package/src/desk/series.ts +127 -0
- package/src/desk/state.ts +77 -0
- package/src/desk/studio-route.ts +119 -0
- package/src/desk/tick-runner.ts +66 -0
- package/src/desk/tick.ts +56 -0
- package/src/desk/topics-route.ts +41 -0
- package/src/desk/ui/app.css +505 -0
- package/src/desk/ui/app.js +185 -0
- package/src/desk/ui/apple-icon.png +0 -0
- package/src/desk/ui/components.js +79 -0
- package/src/desk/ui/core.js +156 -0
- package/src/desk/ui/favicon.ico +0 -0
- package/src/desk/ui/icon.svg +1 -0
- package/src/desk/ui/icons.js +49 -0
- package/src/desk/ui/index.html +41 -0
- package/src/desk/ui/metrics-math.d.ts +22 -0
- package/src/desk/ui/metrics-math.js +135 -0
- package/src/desk/ui/theme.js +5 -0
- package/src/desk/ui/views-creators.js +209 -0
- package/src/desk/ui/views-keys.js +114 -0
- package/src/desk/ui/views-mail.js +188 -0
- package/src/desk/ui/views-misc.js +123 -0
- package/src/desk/ui/views-review.js +119 -0
- package/src/desk/ui/views-runs.js +350 -0
- package/src/desk/ui/views-screens.js +163 -0
- package/src/desk/ui/views-series.js +318 -0
- package/src/desk/ui/views-topics.js +87 -0
- package/src/desk/verbs.ts +361 -0
- package/src/doctor.ts +212 -0
- package/src/drain.ts +14 -0
- package/src/engage/index.ts +217 -0
- package/src/generate/index.ts +110 -0
- package/src/generate/script.ts +152 -0
- package/src/generate/title.ts +78 -0
- package/src/help.ts +233 -0
- package/src/home.ts +45 -0
- package/src/llm/client.ts +177 -0
- package/src/log.ts +7 -0
- package/src/oauth/tokens.ts +189 -0
- package/src/oauth/vault.ts +72 -0
- package/src/pipeline/review.ts +642 -0
- package/src/pipeline/run.ts +443 -0
- package/src/pipeline/series-guard.ts +26 -0
- package/src/pipeline/source-fetch.ts +69 -0
- package/src/pipeline/stages.ts +728 -0
- package/src/pipeline/topics-source.ts +90 -0
- package/src/product/commands.ts +296 -0
- package/src/product/config-file.ts +38 -0
- package/src/product/engine-choice.ts +89 -0
- package/src/product/engines.ts +103 -0
- package/src/product/insights.ts +50 -0
- package/src/product/keys.ts +67 -0
- package/src/product/paths.ts +18 -0
- package/src/product/probes.ts +88 -0
- package/src/product/push-flow.ts +90 -0
- package/src/product/render-video.ts +21 -0
- package/src/product/short.ts +62 -0
- package/src/product/update.ts +32 -0
- package/src/project/canonical.ts +14 -0
- package/src/project/commands.ts +177 -0
- package/src/project/dashboard-status.ts +28 -0
- package/src/project/discover.ts +19 -0
- package/src/project/index-file.ts +73 -0
- package/src/project/marker.ts +87 -0
- package/src/project/migrate.ts +201 -0
- package/src/project/names.ts +27 -0
- package/src/project/notice.ts +16 -0
- package/src/project/open.ts +88 -0
- package/src/project/scaffold.ts +112 -0
- package/src/project/templates.ts +42 -0
- package/src/project/videos.ts +64 -0
- package/src/publish/facebook.ts +91 -0
- package/src/publish/index.ts +12 -0
- package/src/publish/instagram.ts +97 -0
- package/src/publish/rate.ts +62 -0
- package/src/publish/session.ts +242 -0
- package/src/publish/tiktok.ts +202 -0
- package/src/publish/youtube.ts +177 -0
- package/src/queue/desk-ops.ts +1170 -0
- package/src/queue/seal.ts +3 -0
- package/src/queue/store.ts +1006 -0
- package/src/remotion/AiLabel.tsx +26 -0
- package/src/remotion/Root.tsx +73 -0
- package/src/remotion/defaults.ts +26 -0
- package/src/remotion/films/claude/ClaudeFilm.tsx +1014 -0
- package/src/remotion/films/engine/EngineMark.tsx +17 -0
- package/src/remotion/films/rea/ReaShort.tsx +552 -0
- package/src/remotion/films/rea/drawings.tsx +907 -0
- package/src/remotion/films/rea/timing.ts +144 -0
- package/src/remotion/index.ts +4 -0
- package/src/remotion/templates/EditorialSplit.tsx +39 -0
- package/src/remotion/templates/KineticType.tsx +39 -0
- package/src/remotion/templates/SignalBoard.tsx +50 -0
- package/src/remotion/timing.ts +22 -0
- package/src/render/chrome.ts +78 -0
- package/src/render/engine-mark.ts +29 -0
- package/src/render/framey-template.ts +130 -0
- package/src/render/html-engine.ts +149 -0
- package/src/render/node-crypto-stub.ts +11 -0
- package/src/render/probe.ts +29 -0
- package/src/render/props.ts +17 -0
- package/src/render/vertical-short.ts +35 -0
- package/src/render/webpack.ts +33 -0
- package/src/render/worker.ts +65 -0
- package/src/scene/assets/fonts/TitanOne-OFL.txt +94 -0
- package/src/scene/assets/fonts/titan-one.woff2 +0 -0
- package/src/scene/assets/gsap/LICENSE.txt +8 -0
- package/src/scene/assets/gsap/gsap.min.js +11 -0
- package/src/scene/audio.ts +54 -0
- package/src/scene/blocks/LICENSE +190 -0
- package/src/scene/blocks/NOTICE +5 -0
- package/src/scene/blocks/bar-chart-race/bar-chart-race.html +585 -0
- package/src/scene/blocks/beat-freeze-cut/beat-freeze-cut.html +874 -0
- package/src/scene/blocks/camcorder-hud/camcorder-hud.html +164 -0
- package/src/scene/blocks/camera-dolly-zoom/camera-dolly-zoom.html +355 -0
- package/src/scene/blocks/code-diff/code-diff.html +817 -0
- package/src/scene/blocks/code-snippet-apple-terminal-pro/code-snippet-apple-terminal-pro.html +248 -0
- package/src/scene/blocks/code-typing/code-typing.html +764 -0
- package/src/scene/blocks/data-chart/data-chart.html +452 -0
- package/src/scene/blocks/flowchart-vertical/flowchart-vertical.html +454 -0
- package/src/scene/blocks/hw-scribble-transition/hw-scribble-transition.html +153 -0
- package/src/scene/blocks/index.json +824 -0
- package/src/scene/blocks/lt-accent-underline/lt-accent-underline.html +107 -0
- package/src/scene/blocks/lt-bold-block/lt-bold-block.html +109 -0
- package/src/scene/blocks/lt-clean-bar/lt-clean-bar.html +112 -0
- package/src/scene/blocks/lt-dark-card/lt-dark-card.html +111 -0
- package/src/scene/blocks/lt-kicker-name/lt-kicker-name.html +114 -0
- package/src/scene/blocks/lt-mask-reveal/lt-mask-reveal.html +120 -0
- package/src/scene/blocks/lt-neon-border/lt-neon-border.html +391 -0
- package/src/scene/blocks/lt-stack-bars/lt-stack-bars.html +109 -0
- package/src/scene/blocks/macos-notification/macos-notification.html +194 -0
- package/src/scene/blocks/mk-background/mk-background.html +236 -0
- package/src/scene/blocks/mk-callout-highlight/mk-callout-highlight.html +183 -0
- package/src/scene/blocks/mk-clone-wall-transition/mk-clone-wall-transition.html +226 -0
- package/src/scene/blocks/mk-line-graph/mk-line-graph.html +293 -0
- package/src/scene/blocks/mk-progress-stat/mk-progress-stat.html +186 -0
- package/src/scene/blocks/mk-specs-list/mk-specs-list.html +223 -0
- package/src/scene/blocks/news-ticker/news-ticker.html +409 -0
- package/src/scene/blocks/organic-light-leak-overlay/organic-light-leak-overlay.html +126 -0
- package/src/scene/blocks/oscilloscope-trace/oscilloscope-trace.html +519 -0
- package/src/scene/blocks/snow/snow.html +205 -0
- package/src/scene/blocks/split-flap-board/split-flap-board.html +347 -0
- package/src/scene/blocks/spotify-card/spotify-card.html +294 -0
- package/src/scene/blocks/starfield/starfield.html +188 -0
- package/src/scene/blocks/thread-message-stack/thread-message-stack.html +239 -0
- package/src/scene/blocks/week-in-merges/week-in-merges.html +470 -0
- package/src/scene/blocks/x-post/x-post.html +437 -0
- package/src/scene/blocks/yt-comment-card/yt-comment-card.html +319 -0
- package/src/scene/blocks/yt-lcd-background/yt-lcd-background.html +307 -0
- package/src/scene/blocks/yt-prism-title/yt-prism-title.html +205 -0
- package/src/scene/blocks.ts +229 -0
- package/src/scene/command.ts +272 -0
- package/src/scene/figure.ts +181 -0
- package/src/scene/glyphs.ts +107 -0
- package/src/scene/init.ts +86 -0
- package/src/scene/inspect/css-tree.d.ts +6 -0
- package/src/scene/inspect/page-scripts.ts +272 -0
- package/src/scene/inspect/report.ts +405 -0
- package/src/scene/inspect/run.ts +200 -0
- package/src/scene/inspect/source.ts +236 -0
- package/src/scene/label.ts +131 -0
- package/src/scene/net-guard.ts +74 -0
- package/src/scene/render.ts +292 -0
- package/src/scene/runtime/browser.ts +199 -0
- package/src/scene/runtime/core.ts +181 -0
- package/src/scene/runtime/react.tsx +115 -0
- package/src/scene/runtime/transitions.ts +133 -0
- package/src/scene/sanitize.ts +181 -0
- package/src/scene/scene.ts +152 -0
- package/src/scene/studio-command.ts +48 -0
- package/src/scene/studio-skin.ts +59 -0
- package/src/scene/studio.ts +337 -0
- package/src/scene/supply.ts +111 -0
- package/src/scene/timing.ts +288 -0
- package/src/scene/transitions/LICENSE +190 -0
- package/src/scene/transitions/NOTICE +5 -0
- package/src/scene/transitions/SOURCE.json +24 -0
- package/src/scene/transitions/transitions.global.js +38 -0
- package/src/scene/transitions.ts +26 -0
- package/src/service/all-projects.ts +153 -0
- package/src/service/commands.ts +256 -0
- package/src/service/due.ts +82 -0
- package/src/service/lock.ts +101 -0
- package/src/service/log-file.ts +56 -0
- package/src/service/state.ts +41 -0
- package/src/service/units.ts +90 -0
- package/src/service/worker.ts +51 -0
- package/src/skill-agents.ts +74 -0
- package/src/skill-install.ts +334 -0
- package/src/sync-write.ts +21 -0
- package/src/tick-report.ts +205 -0
- package/src/tts.ts +124 -0
- package/vendor/@vfrey/contracts/dist/account.d.ts +97 -0
- package/vendor/@vfrey/contracts/dist/account.js +27 -0
- package/vendor/@vfrey/contracts/dist/checks.d.ts +8 -0
- package/vendor/@vfrey/contracts/dist/checks.js +1 -0
- package/vendor/@vfrey/contracts/dist/credentials/crypto.d.ts +4 -0
- package/vendor/@vfrey/contracts/dist/credentials/crypto.js +38 -0
- package/vendor/@vfrey/contracts/dist/domain/advice-persona.d.ts +10 -0
- package/vendor/@vfrey/contracts/dist/domain/advice-persona.js +24 -0
- package/vendor/@vfrey/contracts/dist/domain/backoff.d.ts +13 -0
- package/vendor/@vfrey/contracts/dist/domain/backoff.js +27 -0
- package/vendor/@vfrey/contracts/dist/domain/cadence.d.ts +29 -0
- package/vendor/@vfrey/contracts/dist/domain/cadence.js +95 -0
- package/vendor/@vfrey/contracts/dist/domain/creators.d.ts +146 -0
- package/vendor/@vfrey/contracts/dist/domain/creators.js +2 -0
- package/vendor/@vfrey/contracts/dist/domain/disclosure.d.ts +127 -0
- package/vendor/@vfrey/contracts/dist/domain/disclosure.js +250 -0
- package/vendor/@vfrey/contracts/dist/domain/errors.d.ts +34 -0
- package/vendor/@vfrey/contracts/dist/domain/errors.js +68 -0
- package/vendor/@vfrey/contracts/dist/domain/redact.d.ts +1 -0
- package/vendor/@vfrey/contracts/dist/domain/redact.js +21 -0
- package/vendor/@vfrey/contracts/dist/domain/revenue.d.ts +13 -0
- package/vendor/@vfrey/contracts/dist/domain/revenue.js +16 -0
- package/vendor/@vfrey/contracts/dist/domain/safety.d.ts +20 -0
- package/vendor/@vfrey/contracts/dist/domain/safety.js +61 -0
- package/vendor/@vfrey/contracts/dist/domain/similarity.d.ts +13 -0
- package/vendor/@vfrey/contracts/dist/domain/similarity.js +100 -0
- package/vendor/@vfrey/contracts/dist/domain/templates.d.ts +18 -0
- package/vendor/@vfrey/contracts/dist/domain/templates.js +67 -0
- package/vendor/@vfrey/contracts/dist/domain/tiktok-chunks.d.ts +19 -0
- package/vendor/@vfrey/contracts/dist/domain/tiktok-chunks.js +52 -0
- package/vendor/@vfrey/contracts/dist/domain/topics.d.ts +78 -0
- package/vendor/@vfrey/contracts/dist/domain/topics.js +4 -0
- package/vendor/@vfrey/contracts/dist/domain/types.d.ts +216 -0
- package/vendor/@vfrey/contracts/dist/domain/types.js +1 -0
- package/vendor/@vfrey/contracts/dist/domain/variation.d.ts +57 -0
- package/vendor/@vfrey/contracts/dist/domain/variation.js +278 -0
- package/vendor/@vfrey/contracts/dist/events.d.ts +20 -0
- package/vendor/@vfrey/contracts/dist/events.js +1 -0
- package/vendor/@vfrey/contracts/dist/http.d.ts +11 -0
- package/vendor/@vfrey/contracts/dist/http.js +37 -0
- package/vendor/@vfrey/contracts/dist/index.d.ts +22 -0
- package/vendor/@vfrey/contracts/dist/index.js +22 -0
- package/vendor/@vfrey/contracts/dist/log.d.ts +2 -0
- package/vendor/@vfrey/contracts/dist/log.js +10 -0
- package/vendor/@vfrey/contracts/dist/mail.d.ts +55 -0
- package/vendor/@vfrey/contracts/dist/mail.js +1 -0
- package/vendor/@vfrey/contracts/package.json +18 -0
- package/vendor/@vfrey/ingest/dist/index.d.ts +41 -0
- package/vendor/@vfrey/ingest/dist/index.js +221 -0
- package/vendor/@vfrey/ingest/dist/parse.d.ts +5 -0
- package/vendor/@vfrey/ingest/dist/parse.js +165 -0
- package/vendor/@vfrey/ingest/package.json +21 -0
- package/vendor/framey/LICENSE +190 -0
- package/vendor/framey/NOTICE +15 -0
- package/vendor/framey/SOURCE.json +24 -0
- package/vendor/framey/STUDIO-SOURCE.json +9 -0
- package/vendor/framey/bin/framey-localize-fonts.mjs +12 -0
- package/vendor/framey/bin/framey.mjs +16 -0
- package/vendor/framey/dist/add-VMJD5KBS.js +75 -0
- package/vendor/framey/dist/animatedGifPrep-QPKGJ6PM.js +43 -0
- package/vendor/framey/dist/api.d.ts +211 -0
- package/vendor/framey/dist/api.js +76 -0
- package/vendor/framey/dist/appHistory-WW56FZTT.js +31 -0
- package/vendor/framey/dist/asset-WQKGOELE.js +26 -0
- package/vendor/framey/dist/audio-RSJWVWRA.js +1936 -0
- package/vendor/framey/dist/batchRender-3HPICDCT.js +366 -0
- package/vendor/framey/dist/beats-HL2BDUZI.js +273 -0
- package/vendor/framey/dist/benchmark-RWCWHI5Z.js +230 -0
- package/vendor/framey/dist/browser-ECYET3Q2.js +206 -0
- package/vendor/framey/dist/bundleWithLocalizedFonts-ALYBQEQT.js +52 -0
- package/vendor/framey/dist/captureCompositionFrame-4DA6SEH2.js +94 -0
- package/vendor/framey/dist/catalog-5JVDEUWK.js +979 -0
- package/vendor/framey/dist/catch-up-NEBCL5NO.js +106 -0
- package/vendor/framey/dist/check-F5N544ML.js +111 -0
- package/vendor/framey/dist/checkBrowser-LZ66CMJG.js +110 -0
- package/vendor/framey/dist/chunk-2K7Z2HVP.js +372 -0
- package/vendor/framey/dist/chunk-2UZQHDM7.js +323 -0
- package/vendor/framey/dist/chunk-2X4PQEYM.js +34 -0
- package/vendor/framey/dist/chunk-3422KMTD.js +168 -0
- package/vendor/framey/dist/chunk-3FYD4YEK.js +72 -0
- package/vendor/framey/dist/chunk-3GXQPCJC.js +682 -0
- package/vendor/framey/dist/chunk-3LM6YPBR.js +179 -0
- package/vendor/framey/dist/chunk-442JNXRM.js +20 -0
- package/vendor/framey/dist/chunk-4BNN5FGX.js +182 -0
- package/vendor/framey/dist/chunk-4P7NX5BM.js +11532 -0
- package/vendor/framey/dist/chunk-4UMPIPCQ.js +88 -0
- package/vendor/framey/dist/chunk-4YYJOFKS.js +28 -0
- package/vendor/framey/dist/chunk-54HJ63Q2.js +29 -0
- package/vendor/framey/dist/chunk-5ETJX6WM.js +89 -0
- package/vendor/framey/dist/chunk-5SG54JRS.js +68 -0
- package/vendor/framey/dist/chunk-634HSJ3J.js +58 -0
- package/vendor/framey/dist/chunk-66LREXGS.js +48 -0
- package/vendor/framey/dist/chunk-6BEFEWFB.js +25 -0
- package/vendor/framey/dist/chunk-6BNTGNGD.js +286 -0
- package/vendor/framey/dist/chunk-6DDPOHMK.js +140 -0
- package/vendor/framey/dist/chunk-6GV5XSOR.js +214 -0
- package/vendor/framey/dist/chunk-6JR2XUEE.js +127 -0
- package/vendor/framey/dist/chunk-73QVATIM.js +362 -0
- package/vendor/framey/dist/chunk-7FMUTGOH.js +279 -0
- package/vendor/framey/dist/chunk-7JQFJ6JJ.js +291 -0
- package/vendor/framey/dist/chunk-AHPGAESX.js +113 -0
- package/vendor/framey/dist/chunk-AHX34EFP.js +15 -0
- package/vendor/framey/dist/chunk-AJ5CSAEZ.js +487 -0
- package/vendor/framey/dist/chunk-B7RJV74O.js +5047 -0
- package/vendor/framey/dist/chunk-BL4U6U52.js +254 -0
- package/vendor/framey/dist/chunk-BSQ7XANM.js +210 -0
- package/vendor/framey/dist/chunk-BZ56MTDX.js +6 -0
- package/vendor/framey/dist/chunk-C5BO7PMT.js +282 -0
- package/vendor/framey/dist/chunk-CFUCHO4T.js +358 -0
- package/vendor/framey/dist/chunk-CZGHFATT.js +31 -0
- package/vendor/framey/dist/chunk-DLA44CMS.js +5649 -0
- package/vendor/framey/dist/chunk-DPVXBVLL.js +31292 -0
- package/vendor/framey/dist/chunk-DQBY7FRR.js +397 -0
- package/vendor/framey/dist/chunk-EAMLBAHA.js +154 -0
- package/vendor/framey/dist/chunk-EOVYQ3IG.js +143 -0
- package/vendor/framey/dist/chunk-EVMY7IPL.js +612 -0
- package/vendor/framey/dist/chunk-FG4BBFZI.js +122 -0
- package/vendor/framey/dist/chunk-FI4I7CME.js +56 -0
- package/vendor/framey/dist/chunk-FLREUEHW.js +2081 -0
- package/vendor/framey/dist/chunk-FUFP7A7S.js +1126 -0
- package/vendor/framey/dist/chunk-FV3PIKGH.js +56 -0
- package/vendor/framey/dist/chunk-FXMKHHPO.js +123 -0
- package/vendor/framey/dist/chunk-GGZAM5VH.js +1187 -0
- package/vendor/framey/dist/chunk-GKZEVJIB.js +39 -0
- package/vendor/framey/dist/chunk-GMECXFAN.js +57 -0
- package/vendor/framey/dist/chunk-GNEFDLFB.js +621 -0
- package/vendor/framey/dist/chunk-GPMLSOEG.js +19274 -0
- package/vendor/framey/dist/chunk-GPYVT5EE.js +59 -0
- package/vendor/framey/dist/chunk-H6IEMZEV.js +40 -0
- package/vendor/framey/dist/chunk-HEFQCIXR.js +170 -0
- package/vendor/framey/dist/chunk-HFQ4CWHA.js +118 -0
- package/vendor/framey/dist/chunk-HPDTCOKE.js +304 -0
- package/vendor/framey/dist/chunk-HTUHQKDB.js +548 -0
- package/vendor/framey/dist/chunk-HU2SPFNG.js +103 -0
- package/vendor/framey/dist/chunk-HVW7ABXD.js +19 -0
- package/vendor/framey/dist/chunk-IG7NDKXO.js +26 -0
- package/vendor/framey/dist/chunk-II3J3WXK.js +246 -0
- package/vendor/framey/dist/chunk-IR35BCLK.js +13 -0
- package/vendor/framey/dist/chunk-ISN5GRYI.js +20 -0
- package/vendor/framey/dist/chunk-IUONB55J.js +669 -0
- package/vendor/framey/dist/chunk-IVSUQR2B.js +161 -0
- package/vendor/framey/dist/chunk-IXU6PQSW.js +259 -0
- package/vendor/framey/dist/chunk-IZCW2NKT.js +29 -0
- package/vendor/framey/dist/chunk-JGXJU5PF.js +68 -0
- package/vendor/framey/dist/chunk-JHOCK3DG.js +27 -0
- package/vendor/framey/dist/chunk-JVBRJMOP.js +121 -0
- package/vendor/framey/dist/chunk-K4MJX7SR.js +17 -0
- package/vendor/framey/dist/chunk-K4WKGXRC.js +18 -0
- package/vendor/framey/dist/chunk-KGPLIH7B.js +127 -0
- package/vendor/framey/dist/chunk-KLIKCX6Q.js +63 -0
- package/vendor/framey/dist/chunk-KXTKKBOC.js +61 -0
- package/vendor/framey/dist/chunk-KZI44OPA.js +185 -0
- package/vendor/framey/dist/chunk-LEKZNRKI.js +45 -0
- package/vendor/framey/dist/chunk-LFGJWOVA.js +61 -0
- package/vendor/framey/dist/chunk-LLMSX6V6.js +17 -0
- package/vendor/framey/dist/chunk-LTGTN2TH.js +195 -0
- package/vendor/framey/dist/chunk-M2WBOQ2Q.js +54 -0
- package/vendor/framey/dist/chunk-MI5FTLYK.js +399 -0
- package/vendor/framey/dist/chunk-MLQP2QUF.js +1809 -0
- package/vendor/framey/dist/chunk-MMQ52NIZ.js +956 -0
- package/vendor/framey/dist/chunk-ND46XKK3.js +742 -0
- package/vendor/framey/dist/chunk-NYEXNSHM.js +504 -0
- package/vendor/framey/dist/chunk-O4YRB27L.js +57 -0
- package/vendor/framey/dist/chunk-O7BGSQ5F.js +92 -0
- package/vendor/framey/dist/chunk-OFQWTKPW.js +117 -0
- package/vendor/framey/dist/chunk-P5ZBLXJ6.js +91 -0
- package/vendor/framey/dist/chunk-P6LJ4Y32.js +4662 -0
- package/vendor/framey/dist/chunk-PKZ7RK5N.js +281 -0
- package/vendor/framey/dist/chunk-PURHAXFL.js +15 -0
- package/vendor/framey/dist/chunk-Q5X2B5WT.js +29 -0
- package/vendor/framey/dist/chunk-QAMKOG67.js +248 -0
- package/vendor/framey/dist/chunk-R6YBK4XX.js +514 -0
- package/vendor/framey/dist/chunk-RGLQURIX.js +2997 -0
- package/vendor/framey/dist/chunk-RNV3DM4S.js +3573 -0
- package/vendor/framey/dist/chunk-ROEW726S.js +4638 -0
- package/vendor/framey/dist/chunk-RQVDTPGD.js +6 -0
- package/vendor/framey/dist/chunk-RVZHGOGR.js +26 -0
- package/vendor/framey/dist/chunk-RXATE6FZ.js +385 -0
- package/vendor/framey/dist/chunk-S3S4TGEH.js +424 -0
- package/vendor/framey/dist/chunk-SDYDNHEE.js +11202 -0
- package/vendor/framey/dist/chunk-SSWBU67I.js +173 -0
- package/vendor/framey/dist/chunk-ST35C4K6.js +28 -0
- package/vendor/framey/dist/chunk-SZNTYTIV.js +990 -0
- package/vendor/framey/dist/chunk-T2EUWCJE.js +29 -0
- package/vendor/framey/dist/chunk-T6AGUTZJ.js +444 -0
- package/vendor/framey/dist/chunk-THJALHFJ.js +11131 -0
- package/vendor/framey/dist/chunk-TIQB63MX.js +18128 -0
- package/vendor/framey/dist/chunk-U242RTCT.js +134 -0
- package/vendor/framey/dist/chunk-UJZREAV6.js +3901 -0
- package/vendor/framey/dist/chunk-UMD3V7CU.js +264 -0
- package/vendor/framey/dist/chunk-UOFAE5RF.js +42 -0
- package/vendor/framey/dist/chunk-UTEIUGAZ.js +29 -0
- package/vendor/framey/dist/chunk-VAS6HQX3.js +138 -0
- package/vendor/framey/dist/chunk-VC76R6W2.js +27 -0
- package/vendor/framey/dist/chunk-VFZ7W5W7.js +226 -0
- package/vendor/framey/dist/chunk-VHNLZAE2.js +348 -0
- package/vendor/framey/dist/chunk-VMC4WSH5.js +278 -0
- package/vendor/framey/dist/chunk-VORQIH64.js +108 -0
- package/vendor/framey/dist/chunk-VTXFRIHO.js +186 -0
- package/vendor/framey/dist/chunk-W2LGJSEN.js +182 -0
- package/vendor/framey/dist/chunk-WKBM7NDG.js +32 -0
- package/vendor/framey/dist/chunk-WMQNIQDJ.js +2057 -0
- package/vendor/framey/dist/chunk-WN4XZC5F.js +504 -0
- package/vendor/framey/dist/chunk-WQ6ZXHFQ.js +133 -0
- package/vendor/framey/dist/chunk-X7HBSTCZ.js +53 -0
- package/vendor/framey/dist/chunk-X7MTZ4T2.js +6 -0
- package/vendor/framey/dist/chunk-XPTOJZJF.js +308 -0
- package/vendor/framey/dist/chunk-XT6MXC7A.js +6 -0
- package/vendor/framey/dist/chunk-XT7X6QCP.js +6 -0
- package/vendor/framey/dist/chunk-XTMDWNEC.js +2820 -0
- package/vendor/framey/dist/chunk-XUHOIDKG.js +6 -0
- package/vendor/framey/dist/chunk-XX7LTW5Y.js +138 -0
- package/vendor/framey/dist/chunk-Y3YGTZEG.js +382 -0
- package/vendor/framey/dist/chunk-Y5SBSGN7.js +346 -0
- package/vendor/framey/dist/chunk-YCMVI4MZ.js +625 -0
- package/vendor/framey/dist/chunk-YOR4CTCE.js +261 -0
- package/vendor/framey/dist/chunk-YPRBGITP.js +2748 -0
- package/vendor/framey/dist/chunk-ZKDMAOFU.js +32 -0
- package/vendor/framey/dist/chunk-ZW6DURPF.js +1318 -0
- package/vendor/framey/dist/clean-ZJOGDKDM.js +336 -0
- package/vendor/framey/dist/cli.js +439 -0
- package/vendor/framey/dist/cloudrun-HGJ3B6ZV.js +767 -0
- package/vendor/framey/dist/commands/contrast-audit.browser.js +643 -0
- package/vendor/framey/dist/commands/layout-audit.browser.js +2914 -0
- package/vendor/framey/dist/commands/motion-sample.browser.js +74 -0
- package/vendor/framey/dist/commands/motion-signature.browser.js +475 -0
- package/vendor/framey/dist/compare-W7UY3BAZ.js +463 -0
- package/vendor/framey/dist/compiler-TM3W6GYF.js +136 -0
- package/vendor/framey/dist/component-QF5VGYMU.js +176 -0
- package/vendor/framey/dist/compositions-XT3PMNDF.js +238 -0
- package/vendor/framey/dist/contactSheet-7W6GYHBQ.js +24 -0
- package/vendor/framey/dist/deploy-6FJPWKF2.js +299 -0
- package/vendor/framey/dist/destroy-HE2RIPKZ.js +57 -0
- package/vendor/framey/dist/deterministicFonts-RADD3BSX.js +50 -0
- package/vendor/framey/dist/dist-4S2FGGVY.js +422 -0
- package/vendor/framey/dist/dist-BE4B2EZE.js +32 -0
- package/vendor/framey/dist/dist-EWXJGN63.js +129 -0
- package/vendor/framey/dist/docs-VKIRFEEX.js +131 -0
- package/vendor/framey/dist/doctor-2UNJ2G7U.js +74 -0
- package/vendor/framey/dist/dom-37K6YOH4.js +14 -0
- package/vendor/framey/dist/dotEnv-7PZQTVIM.js +54 -0
- package/vendor/framey/dist/events-ACY3P2PA.js +76 -0
- package/vendor/framey/dist/events-IHB3B2MK.js +69 -0
- package/vendor/framey/dist/ffmpeg-DR5LGGSK.js +63 -0
- package/vendor/framey/dist/figma-BODVUPHB.js +63 -0
- package/vendor/framey/dist/fontData.generated-NXAOI4GH.js +13 -0
- package/vendor/framey/dist/fontLocalizeCli.js +142 -0
- package/vendor/framey/dist/gpuPolicy-XLEXORZY.js +49 -0
- package/vendor/framey/dist/grade-compare-GAXFQ74C.js +661 -0
- package/vendor/framey/dist/gsapParser-IEUSLYDJ.js +2885 -0
- package/vendor/framey/dist/gsapParserAcorn-LZJKCFQI.js +30 -0
- package/vendor/framey/dist/help-VCB5X2HS.js +184 -0
- package/vendor/framey/dist/history-ENGZEVFT.js +471 -0
- package/vendor/framey/dist/hyperframe.manifest.json +22 -0
- package/vendor/framey/dist/hyperframe.runtime.iife.js +1132 -0
- package/vendor/framey/dist/hyperframes-player.global.js +478 -0
- package/vendor/framey/dist/hyperframes-slideshow.global.js +148 -0
- package/vendor/framey/dist/info-WPCJHBMU.js +182 -0
- package/vendor/framey/dist/init-G4WGLYGD.js +1176 -0
- package/vendor/framey/dist/inspect-5IQ6CNEQ.js +106 -0
- package/vendor/framey/dist/installer-ML4HWUV7.js +40 -0
- package/vendor/framey/dist/keyframes-Q6XJO7VW.js +2755 -0
- package/vendor/framey/dist/lambda-A7JC74JF.js +468 -0
- package/vendor/framey/dist/layout-YET6XNXA.js +95 -0
- package/vendor/framey/dist/lint-I7FZN2VJ.js +162 -0
- package/vendor/framey/dist/lintProject-PDB3UN72.js +30 -0
- package/vendor/framey/dist/manager-CD6LJ7ZN.js +45 -0
- package/vendor/framey/dist/manager-WA4F2GST.js +67 -0
- package/vendor/framey/dist/media-treatment-BXYS4WVA.js +1030 -0
- package/vendor/framey/dist/media-use-LWH3MANK.js +113 -0
- package/vendor/framey/dist/models-K363FYM4.js +195 -0
- package/vendor/framey/dist/motionShot-GPAVWXLH.js +640 -0
- package/vendor/framey/dist/normalize-FASIPBUM.js +24 -0
- package/vendor/framey/dist/normalize-audio-VDWYRHN6.js +268 -0
- package/vendor/framey/dist/open-E3TB6P6J.js +128 -0
- package/vendor/framey/dist/parakeetRunner-UP34UOI2.js +58 -0
- package/vendor/framey/dist/pipeline-VXIUYEBU.js +64 -0
- package/vendor/framey/dist/play-BUTDQ4DV.js +353 -0
- package/vendor/framey/dist/policies-OLDYGFK5.js +309 -0
- package/vendor/framey/dist/preflight-2KZ2GWAZ.js +55 -0
- package/vendor/framey/dist/present-GXYSM7NX.js +353 -0
- package/vendor/framey/dist/preview-EKZ33JOJ.js +1910 -0
- package/vendor/framey/dist/producer-SKU5EV3J.js +13 -0
- package/vendor/framey/dist/progress-ZWB4Z4ET.js +76 -0
- package/vendor/framey/dist/projectConfig-3MZAYVTS.js +60 -0
- package/vendor/framey/dist/publication-DTKWNF74.js +33 -0
- package/vendor/framey/dist/remove-background-OY5L7XN2.js +265 -0
- package/vendor/framey/dist/render-CDV4GDFX.js +187 -0
- package/vendor/framey/dist/render-YE5YAMOD.js +2620 -0
- package/vendor/framey/dist/render-batch-ZAMFPCLL.js +274 -0
- package/vendor/framey/dist/renderSetupWorker.js +120 -0
- package/vendor/framey/dist/resolver-D5BIPSEW.js +42 -0
- package/vendor/framey/dist/runtimeVersion.js +18 -0
- package/vendor/framey/dist/sam-WFZXI4F7.js +23 -0
- package/vendor/framey/dist/shaderTransitionWorker.js +44 -0
- package/vendor/framey/dist/sherpa-RQWLV7I3.js +79 -0
- package/vendor/framey/dist/sherpaWorker.js +84 -0
- package/vendor/framey/dist/sites-AMOVABTK.js +54 -0
- package/vendor/framey/dist/skill-ZHVLH7KX.js +15 -0
- package/vendor/framey/dist/slideshow-SJGK4RQS.js +24 -0
- package/vendor/framey/dist/snapshot-TUXL6LBQ.js +1515 -0
- package/vendor/framey/dist/src-JHQGB2S6.js +215 -0
- package/vendor/framey/dist/src-Z5PGEMXB.js +532 -0
- package/vendor/framey/dist/state-B6H3WHCF.js +28 -0
- package/vendor/framey/dist/staticProjectServer-DFVZOUGI.js +63 -0
- package/vendor/framey/dist/studio/assets/hyperframes-player-DLaKjw0L.js +478 -0
- package/vendor/framey/dist/studio/assets/index-BX3KHhGX.js +71 -0
- package/vendor/framey/dist/studio/assets/index-CYbvOVp-.css +1 -0
- package/vendor/framey/dist/studio/assets/index-Dp3QEUGL.js +402 -0
- package/vendor/framey/dist/studio/assets/index-qrTAXbqc.js +531 -0
- package/vendor/framey/dist/studio/favicon.svg +14 -0
- package/vendor/framey/dist/studio/icons/timeline/audio.svg +7 -0
- package/vendor/framey/dist/studio/icons/timeline/captions.svg +5 -0
- package/vendor/framey/dist/studio/icons/timeline/composition.svg +12 -0
- package/vendor/framey/dist/studio/icons/timeline/image.svg +18 -0
- package/vendor/framey/dist/studio/icons/timeline/music.svg +10 -0
- package/vendor/framey/dist/studio/icons/timeline/text.svg +3 -0
- package/vendor/framey/dist/studio/index.html +23 -0
- package/vendor/framey/dist/studio-api-2NECGRMB.js +179 -0
- package/vendor/framey/dist/studioSelectionClient-IUUPKUHZ.js +25 -0
- package/vendor/framey/dist/studioServer-N2OPUMOQ.js +1569 -0
- package/vendor/framey/dist/synthesize-PKTB5ATG.js +183 -0
- package/vendor/framey/dist/system-7F3UEHQT.js +27 -0
- package/vendor/framey/dist/telemetry-DQGPEKM6.js +145 -0
- package/vendor/framey/dist/telemetry-UN55N6U6.js +77 -0
- package/vendor/framey/dist/timeline-LHUUMK63.js +1295 -0
- package/vendor/framey/dist/tokens-OLECWDFK.js +92 -0
- package/vendor/framey/dist/transcribe-O7HQKMIV.js +331 -0
- package/vendor/framey/dist/transcribe-UJESPBT5.js +76 -0
- package/vendor/framey/dist/tts-YCCQHZD4.js +234 -0
- package/vendor/framey/dist/urlDownloader-H2LXLG4U.js +48 -0
- package/vendor/framey/dist/usage-HG4WMM7A.js +2167 -0
- package/vendor/framey/dist/validate-F2XMN6EV.js +87 -0
- package/vendor/framey/dist/vendor/npm/gsap@3.15.0/GSAP-LICENSE.txt +8 -0
- package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/CustomEase.min.js +11 -0
- package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/MotionPathPlugin.min.js +11 -0
- package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/gsap.min.js +11 -0
- package/vendor/framey/dist/warmCheck.js +220 -0
- package/vendor/framey/dist/warmCheckClient.js +139 -0
- package/vendor/framey/dist/workspaceBuildCheck-XYD2YTFW.js +133 -0
- package/vendor/framey-player/LICENSE +190 -0
- package/vendor/framey-player/NOTICE +16 -0
- package/vendor/framey-player/SOURCE.json +6 -0
- package/vendor/framey-player/hyperframes-player.global.js +478 -0
- package/vendor/framey.pin +1 -0
- package/bin/index.js +0 -3
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { canonicalPath } from "./canonical.js";
|
|
4
|
+
|
|
5
|
+
export interface IndexEntry {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
root: string;
|
|
9
|
+
lastUsed: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const INDEX_FILE = "projects.json";
|
|
13
|
+
|
|
14
|
+
const indexFile = (machineHome: string) => path.join(machineHome, INDEX_FILE);
|
|
15
|
+
|
|
16
|
+
/** The machine index. Never the source of truth for a project's data; a missing or broken file reads as empty. */
|
|
17
|
+
export function readIndex(machineHome: string): IndexEntry[] {
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(readFileSync(indexFile(machineHome), "utf8")) as unknown;
|
|
20
|
+
if (!Array.isArray(parsed)) return [];
|
|
21
|
+
const valid = parsed.filter(
|
|
22
|
+
(item): item is IndexEntry =>
|
|
23
|
+
!!item && typeof item === "object" && typeof item.id === "string" && typeof item.name === "string" && typeof item.root === "string" && typeof item.lastUsed === "string",
|
|
24
|
+
);
|
|
25
|
+
// Entries written through a symlinked path are read as their real root; two that meet there are one project (newest wins).
|
|
26
|
+
const byRoot = new Map<string, IndexEntry>();
|
|
27
|
+
for (const item of valid) {
|
|
28
|
+
const root = canonicalPath(item.root);
|
|
29
|
+
const seen = byRoot.get(root);
|
|
30
|
+
if (!seen || item.lastUsed >= seen.lastUsed) byRoot.set(root, { ...item, root });
|
|
31
|
+
}
|
|
32
|
+
return [...byRoot.values()];
|
|
33
|
+
} catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function writeIndex(machineHome: string, entries: IndexEntry[]): void {
|
|
39
|
+
mkdirSync(machineHome, { recursive: true });
|
|
40
|
+
const file = indexFile(machineHome);
|
|
41
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
42
|
+
writeFileSync(tmp, JSON.stringify(entries, null, 2) + "\n");
|
|
43
|
+
renameSync(tmp, file);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Adds the project, or refreshes its name, root and time when its id or root is already listed. */
|
|
47
|
+
export function registerProject(machineHome: string, project: { id: string; name: string; root: string }, now: Date = new Date()): void {
|
|
48
|
+
const root = canonicalPath(project.root);
|
|
49
|
+
const entries = readIndex(machineHome).filter((entry) => entry.id !== project.id && entry.root !== root);
|
|
50
|
+
entries.push({ id: project.id, name: project.name, root, lastUsed: now.toISOString() });
|
|
51
|
+
writeIndex(machineHome, entries);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A command resolved this project: note it. Never fails the command. */
|
|
55
|
+
export function touchProject(machineHome: string, project: { id: string; name: string; root: string }, now: Date = new Date()): void {
|
|
56
|
+
try {
|
|
57
|
+
registerProject(machineHome, project, now);
|
|
58
|
+
} catch {
|
|
59
|
+
/* the index is a convenience */
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function isMissing(entry: IndexEntry): boolean {
|
|
64
|
+
return !existsSync(path.join(entry.root, "vfrey.json"));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Drops every entry whose root is gone. Returns what it dropped. */
|
|
68
|
+
export function pruneIndex(machineHome: string): IndexEntry[] {
|
|
69
|
+
const entries = readIndex(machineHome);
|
|
70
|
+
const gone = entries.filter(isMissing);
|
|
71
|
+
if (gone.length > 0) writeIndex(machineHome, entries.filter((entry) => !isMissing(entry)));
|
|
72
|
+
return gone;
|
|
73
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { canonicalPath } from "./canonical.js";
|
|
5
|
+
|
|
6
|
+
import { ENGINE_IDS } from "../product/engines.js";
|
|
7
|
+
|
|
8
|
+
export const MARKER_FILE = "vfrey.json";
|
|
9
|
+
|
|
10
|
+
export interface ProjectMarker {
|
|
11
|
+
version: 1;
|
|
12
|
+
name: string;
|
|
13
|
+
id: string;
|
|
14
|
+
/** This project's preferred render engine: beats the machine default, loses to `--engine` and a series engine. */
|
|
15
|
+
engine?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A vfrey project: the folder that holds `vfrey.json`. */
|
|
19
|
+
export interface Project {
|
|
20
|
+
name: string;
|
|
21
|
+
id: string;
|
|
22
|
+
/** The engine named in vfrey.json, if any. */
|
|
23
|
+
engine?: string;
|
|
24
|
+
root: string;
|
|
25
|
+
/** `<root>/.vfrey`, where the database, logs and per-job copies live. */
|
|
26
|
+
dataDir: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A broken `vfrey.json`. The CLI prints it in the refusal style. */
|
|
30
|
+
export class ProjectError extends Error {
|
|
31
|
+
constructor(
|
|
32
|
+
readonly reason: string,
|
|
33
|
+
readonly fix: string,
|
|
34
|
+
) {
|
|
35
|
+
super(`${reason} Fix: ${fix}`);
|
|
36
|
+
this.name = "ProjectError";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function validateMarker(value: unknown): ProjectMarker | string {
|
|
41
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "it is not a JSON object";
|
|
42
|
+
const marker = value as Record<string, unknown>;
|
|
43
|
+
if (marker.version !== 1) return `"version" must be 1`;
|
|
44
|
+
if (typeof marker.name !== "string" || !marker.name.trim()) return `"name" must be a non-empty string`;
|
|
45
|
+
if (typeof marker.id !== "string" || !marker.id.trim()) return `"id" must be a non-empty string`;
|
|
46
|
+
if (marker.engine !== undefined && !(typeof marker.engine === "string" && (ENGINE_IDS as readonly string[]).includes(marker.engine))) {
|
|
47
|
+
return `"engine" must be one of ${ENGINE_IDS.join(", ")}`;
|
|
48
|
+
}
|
|
49
|
+
return { ...marker, version: 1, name: marker.name.trim(), id: marker.id.trim() } as ProjectMarker;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Reads and checks `<root>/vfrey.json`. Throws ProjectError when the file is there but wrong. */
|
|
53
|
+
export function readMarker(root: string): ProjectMarker {
|
|
54
|
+
const file = path.join(root, MARKER_FILE);
|
|
55
|
+
let parsed: unknown;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
58
|
+
} catch {
|
|
59
|
+
throw new ProjectError(`${file} is not valid JSON.`, `fix the file, or delete it if this folder is not a vfrey project.`);
|
|
60
|
+
}
|
|
61
|
+
const checked = validateMarker(parsed);
|
|
62
|
+
if (typeof checked === "string") {
|
|
63
|
+
throw new ProjectError(
|
|
64
|
+
`${file} is not a vfrey project file: ${checked}.`,
|
|
65
|
+
checked.startsWith('"engine"') ? `set "engine" to one of ${ENGINE_IDS.join(", ")}, or remove it to follow the machine default.` : `use {"version": 1, "name": "my-channel", "id": "<uuid>"}.`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return checked;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function writeMarker(root: string, marker: ProjectMarker): void {
|
|
72
|
+
writeFileSync(path.join(root, MARKER_FILE), JSON.stringify(marker, null, 2) + "\n");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function newMarker(name: string): ProjectMarker {
|
|
76
|
+
return { version: 1, name, id: randomUUID() };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function hasMarker(root: string): boolean {
|
|
80
|
+
return existsSync(path.join(root, MARKER_FILE));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function projectAt(rootPath: string): Project {
|
|
84
|
+
const root = canonicalPath(rootPath);
|
|
85
|
+
const marker = readMarker(root);
|
|
86
|
+
return { name: marker.name, id: marker.id, ...(marker.engine ? { engine: marker.engine } : {}), root, dataDir: path.join(root, ".vfrey") };
|
|
87
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { cpSync, existsSync, mkdirSync, mkdtempSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { DatabaseSync } from "node:sqlite";
|
|
4
|
+
import { Store } from "../queue/store.js";
|
|
5
|
+
import { canonicalPath } from "./canonical.js";
|
|
6
|
+
import { ProjectError, type Project } from "./marker.js";
|
|
7
|
+
import { readableVideoPath } from "./names.js";
|
|
8
|
+
import { videoFile } from "./videos.js";
|
|
9
|
+
|
|
10
|
+
export interface ImportCounts {
|
|
11
|
+
/** Jobs newly copied into the project's database this run. */
|
|
12
|
+
jobs: number;
|
|
13
|
+
/** Rows copied in all other tables. */
|
|
14
|
+
otherRows: number;
|
|
15
|
+
videos: number;
|
|
16
|
+
transcripts: number;
|
|
17
|
+
scenes: number;
|
|
18
|
+
/** Runs whose video was already in the project from an earlier import. */
|
|
19
|
+
alreadyImported: number;
|
|
20
|
+
/** Runs that never had a video file on disk. */
|
|
21
|
+
withoutVideo: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type Payload = {
|
|
25
|
+
content?: { title?: unknown; outputPath?: unknown; voiceoverPath?: unknown; voiceoverScript?: unknown } | null;
|
|
26
|
+
scene?: { source?: unknown; dir?: unknown } | null;
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const quote = (name: string) => `"${name.replace(/"/g, '""')}"`;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Copies a machine desk (a folder with `pipeline.sqlite`, `renders/` and `scenes/`) into a project: the database rows with
|
|
34
|
+
* their job ids, each render as `videos/YYYY-MM-DD <title>.mp4` with its transcript, and the stored scene copies into
|
|
35
|
+
* `.vfrey/scenes/`. Stored video paths become project-relative. The source is never written: the database is read through
|
|
36
|
+
* a consistent `VACUUM INTO` copy, and a source another process is writing is refused. Running it again adds nothing.
|
|
37
|
+
*/
|
|
38
|
+
export function importDesk(sourceArg: string, project: Project): ImportCounts {
|
|
39
|
+
const source = canonicalPath(sourceArg);
|
|
40
|
+
const sourceDb = path.join(source, "pipeline.sqlite");
|
|
41
|
+
if (!existsSync(sourceDb)) throw new ProjectError(`${source} has no pipeline.sqlite, so there is nothing to import.`, "pass the data folder, for example `--from ~/.vfrey`.");
|
|
42
|
+
if (canonicalPath(project.dataDir) === source) throw new ProjectError("--from points at this project's own data folder.", "pass the machine desk or another data folder.");
|
|
43
|
+
|
|
44
|
+
const counts: ImportCounts = { jobs: 0, otherRows: 0, videos: 0, transcripts: 0, scenes: 0, alreadyImported: 0, withoutVideo: 0 };
|
|
45
|
+
mkdirSync(project.dataDir, { recursive: true });
|
|
46
|
+
const work = mkdtempSync(path.join(project.dataDir, "import-"));
|
|
47
|
+
const copy = path.join(work, "source.sqlite");
|
|
48
|
+
try {
|
|
49
|
+
snapshot(sourceDb, copy, source);
|
|
50
|
+
const store = new Store(path.join(project.dataDir, "pipeline.sqlite"));
|
|
51
|
+
try {
|
|
52
|
+
const ids = mergeTables(store.db, copy, counts);
|
|
53
|
+
moveFiles(store.db, ids, source, project, counts);
|
|
54
|
+
} finally {
|
|
55
|
+
store.close();
|
|
56
|
+
}
|
|
57
|
+
} finally {
|
|
58
|
+
rmSync(work, { recursive: true, force: true });
|
|
59
|
+
}
|
|
60
|
+
return counts;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A consistent copy of the source database. Refuses when another process holds the write lock. */
|
|
64
|
+
function snapshot(sourceDb: string, dest: string, source: string): void {
|
|
65
|
+
let db: DatabaseSync | null = null;
|
|
66
|
+
try {
|
|
67
|
+
db = new DatabaseSync(sourceDb);
|
|
68
|
+
db.exec("PRAGMA busy_timeout = 0");
|
|
69
|
+
try {
|
|
70
|
+
db.exec("BEGIN IMMEDIATE");
|
|
71
|
+
db.exec("ROLLBACK");
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (/locked|busy/i.test(String((error as Error).message))) {
|
|
74
|
+
throw new ProjectError(`${source} is busy: another vfrey process is writing to its database.`, "stop its dashboard or worker (or wait for its tick to finish), then run this again.");
|
|
75
|
+
}
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
79
|
+
db.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (error instanceof ProjectError) throw error;
|
|
82
|
+
if (/locked|busy/i.test(String((error as Error).message))) {
|
|
83
|
+
throw new ProjectError(`${source} is busy: another vfrey process is writing to its database.`, "stop its dashboard or worker, then run this again.");
|
|
84
|
+
}
|
|
85
|
+
throw new ProjectError(`${sourceDb} could not be read as a vfrey database (${(error as Error).message}).`, "pass a data folder made by vfrey.");
|
|
86
|
+
} finally {
|
|
87
|
+
try {
|
|
88
|
+
db?.close();
|
|
89
|
+
} catch {
|
|
90
|
+
/* closing a refused connection */
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** INSERT OR IGNORE every table of the copy into the project's database, by shared column names. */
|
|
96
|
+
function mergeTables(db: DatabaseSync, copy: string, counts: ImportCounts): Set<string> {
|
|
97
|
+
db.exec(`ATTACH DATABASE '${copy.replace(/'/g, "''")}' AS src`);
|
|
98
|
+
try {
|
|
99
|
+
const ids = new Set((db.prepare(`SELECT id FROM src.jobs`).all() as Array<{ id: string }>).map((r) => r.id));
|
|
100
|
+
const first = (db.prepare(`SELECT COUNT(*) AS n FROM main.jobs j WHERE j.id IN (SELECT id FROM src.jobs)`).get() as { n: number }).n === 0;
|
|
101
|
+
const tables = (db.prepare(`SELECT name FROM src.sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name`).all() as Array<{ name: string }>).map((t) => t.name);
|
|
102
|
+
db.exec("BEGIN");
|
|
103
|
+
try {
|
|
104
|
+
for (const table of tables) {
|
|
105
|
+
const mine = db.prepare(`PRAGMA main.table_info(${quote(table)})`).all() as Array<{ name: string; pk: number }>;
|
|
106
|
+
if (mine.length === 0) continue;
|
|
107
|
+
const theirs = new Set((db.prepare(`PRAGMA src.table_info(${quote(table)})`).all() as Array<{ name: string }>).map((c) => c.name));
|
|
108
|
+
const columns = mine.map((c) => c.name).filter((c) => theirs.has(c));
|
|
109
|
+
if (columns.length === 0) continue;
|
|
110
|
+
// A table with no key would gain the same rows again on a re-run, so it is copied once, on the first import.
|
|
111
|
+
const keyed = mine.some((c) => c.pk > 0) || (db.prepare(`PRAGMA main.index_list(${quote(table)})`).all() as Array<{ unique: number }>).some((i) => i.unique === 1);
|
|
112
|
+
if (!keyed && !first) continue;
|
|
113
|
+
const list = columns.map(quote).join(", ");
|
|
114
|
+
const before = (db.prepare(`SELECT COUNT(*) AS n FROM main.${quote(table)}`).get() as { n: number }).n;
|
|
115
|
+
db.exec(`INSERT OR IGNORE INTO main.${quote(table)} (${list}) SELECT ${list} FROM src.${quote(table)}`);
|
|
116
|
+
const added = (db.prepare(`SELECT COUNT(*) AS n FROM main.${quote(table)}`).get() as { n: number }).n - before;
|
|
117
|
+
if (table === "jobs") counts.jobs += added;
|
|
118
|
+
else counts.otherRows += added;
|
|
119
|
+
}
|
|
120
|
+
db.exec("COMMIT");
|
|
121
|
+
} catch (error) {
|
|
122
|
+
db.exec("ROLLBACK");
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
return ids;
|
|
126
|
+
} finally {
|
|
127
|
+
db.exec("DETACH DATABASE src");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Copy each pipeline run's video, transcript and scene into the project and rewrite its stored paths. */
|
|
132
|
+
function moveFiles(db: DatabaseSync, ids: Set<string>, source: string, project: Project, counts: ImportCounts): void {
|
|
133
|
+
const videos = path.join(project.root, "videos");
|
|
134
|
+
const scenesOut = path.join(project.dataDir, "scenes");
|
|
135
|
+
mkdirSync(videos, { recursive: true });
|
|
136
|
+
const rows = db.prepare(`SELECT id, payload, finished_at, updated_at, created_at FROM jobs WHERE kind = 'pipeline' ORDER BY created_at, id`).all() as Array<{
|
|
137
|
+
id: string;
|
|
138
|
+
payload: string;
|
|
139
|
+
finished_at: string | null;
|
|
140
|
+
updated_at: string;
|
|
141
|
+
created_at: string;
|
|
142
|
+
}>;
|
|
143
|
+
const update = db.prepare(`UPDATE jobs SET payload = ? WHERE id = ?`);
|
|
144
|
+
for (const row of rows) {
|
|
145
|
+
let payload: Payload;
|
|
146
|
+
try {
|
|
147
|
+
payload = JSON.parse(row.payload) as Payload;
|
|
148
|
+
} catch {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
// Only runs that came from the source: a project's own runs keep their paths.
|
|
152
|
+
if (!ids.has(row.id)) continue;
|
|
153
|
+
let changed = false;
|
|
154
|
+
|
|
155
|
+
const stored = payload.content?.outputPath;
|
|
156
|
+
const relative = typeof stored === "string" && !path.isAbsolute(stored) ? path.resolve(project.root, stored) : null;
|
|
157
|
+
if (relative && existsSync(relative)) {
|
|
158
|
+
counts.alreadyImported += 1;
|
|
159
|
+
} else {
|
|
160
|
+
const mp4 = videoFile(source, row.id, stored);
|
|
161
|
+
if (!mp4) {
|
|
162
|
+
counts.withoutVideo += 1;
|
|
163
|
+
} else {
|
|
164
|
+
const when = new Date(row.finished_at || row.updated_at || row.created_at);
|
|
165
|
+
const title = typeof payload.content?.title === "string" && payload.content.title.trim() ? payload.content.title : "video";
|
|
166
|
+
const dest = readableVideoPath(videos, Number.isNaN(when.getTime()) ? new Date() : when, title);
|
|
167
|
+
cpSync(mp4, dest);
|
|
168
|
+
counts.videos += 1;
|
|
169
|
+
const text = mp4.replace(/\.mp4$/, ".txt");
|
|
170
|
+
const script = typeof payload.content?.voiceoverScript === "string" ? payload.content.voiceoverScript.trim() : "";
|
|
171
|
+
if (existsSync(text)) {
|
|
172
|
+
cpSync(text, dest.replace(/\.mp4$/, ".txt"));
|
|
173
|
+
counts.transcripts += 1;
|
|
174
|
+
} else if (script) {
|
|
175
|
+
writeFileSync(dest.replace(/\.mp4$/, ".txt"), `${script}\n`);
|
|
176
|
+
counts.transcripts += 1;
|
|
177
|
+
}
|
|
178
|
+
payload.content = { ...(payload.content ?? {}), outputPath: path.relative(project.root, dest).split(path.sep).join("/") };
|
|
179
|
+
changed = true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// The voice file stays behind in the source desk; nothing reads it after the render.
|
|
183
|
+
if (payload.content && typeof payload.content.voiceoverPath === "string" && !existsSync(path.resolve(project.root, payload.content.voiceoverPath))) {
|
|
184
|
+
payload.content.voiceoverPath = null;
|
|
185
|
+
changed = true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const sceneFrom = path.join(source, "scenes", row.id);
|
|
189
|
+
const sceneTo = path.join(scenesOut, row.id);
|
|
190
|
+
if (existsSync(sceneFrom) && statSync(sceneFrom).isDirectory() && !existsSync(sceneTo)) {
|
|
191
|
+
mkdirSync(scenesOut, { recursive: true });
|
|
192
|
+
cpSync(sceneFrom, sceneTo, { recursive: true });
|
|
193
|
+
counts.scenes += 1;
|
|
194
|
+
}
|
|
195
|
+
if (payload.scene && payload.scene.source === "agent" && existsSync(sceneTo) && payload.scene.dir !== sceneTo) {
|
|
196
|
+
payload.scene = { ...payload.scene, dir: sceneTo };
|
|
197
|
+
changed = true;
|
|
198
|
+
}
|
|
199
|
+
if (changed) update.run(JSON.stringify(payload), row.id);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const UNSAFE = /[\\/:*?"<>|\u0000-\u001f]/g;
|
|
5
|
+
|
|
6
|
+
/** A title that is safe in a file name on every system: no `/ : \ * ? " < > |`, at most 80 characters. */
|
|
7
|
+
export function safeTitle(title: string): string {
|
|
8
|
+
const cleaned = title.replace(UNSAFE, " ").replace(/\s+/g, " ").trim();
|
|
9
|
+
const cut = Array.from(cleaned).slice(0, 80).join("").trim().replace(/^\.+/, "").replace(/[. ]+$/, "");
|
|
10
|
+
return cut || "video";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The local calendar day as YYYY-MM-DD. */
|
|
14
|
+
export function dayStamp(date: Date): string {
|
|
15
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
16
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** `<videosDir>/YYYY-MM-DD <title>.mp4`, or with ` (2)`, ` (3)` ... when the mp4 or its transcript is already there. */
|
|
20
|
+
export function readableVideoPath(videosDir: string, date: Date, title: string): string {
|
|
21
|
+
const base = `${dayStamp(date)} ${safeTitle(title)}`;
|
|
22
|
+
for (let n = 1; ; n += 1) {
|
|
23
|
+
const stem = n === 1 ? base : `${base} (${n})`;
|
|
24
|
+
const mp4 = path.join(videosDir, `${stem}.mp4`);
|
|
25
|
+
if (!existsSync(mp4) && !existsSync(path.join(videosDir, `${stem}.txt`))) return mp4;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ResolvedHome } from "../home.js";
|
|
2
|
+
|
|
3
|
+
export const NOT_IN_PROJECT_NOTICE = "Not in a vfrey project; using the machine desk ~/.vfrey. Start one: npm create vfrey@latest my-channel\n";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The stderr line for a command that makes or lists videos outside a project. Null in a project, and null when an env var
|
|
7
|
+
* chose the data folder: then "the machine desk ~/.vfrey" would not be true.
|
|
8
|
+
*/
|
|
9
|
+
export function projectNotice(home: ResolvedHome): string | null {
|
|
10
|
+
return home.source === "machine" ? NOT_IN_PROJECT_NOTICE : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The `project` field of a `--json` document. */
|
|
14
|
+
export function projectJson(home: ResolvedHome): { name: string; root: string } | null {
|
|
15
|
+
return home.project ? { name: home.project.name, root: home.project.root } : null;
|
|
16
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { deskOf } from "../dashboard-link.js";
|
|
5
|
+
import { browserDisabled, ensureDashboard, openBrowser, type EnsureOptions, type EnsureResult } from "../dashboard-start.js";
|
|
6
|
+
import { resolveHome } from "../home.js";
|
|
7
|
+
import { Store } from "../queue/store.js";
|
|
8
|
+
import { isMissing, readIndex } from "./index-file.js";
|
|
9
|
+
import { projectJson, projectNotice } from "./notice.js";
|
|
10
|
+
import { listVideos, type VideoRow } from "./videos.js";
|
|
11
|
+
import type { ProjectIo } from "./commands.js";
|
|
12
|
+
|
|
13
|
+
export interface OpenIo extends ProjectIo {
|
|
14
|
+
/** Test hooks. The CLI omits them. */
|
|
15
|
+
ensure?: (options: EnsureOptions) => Promise<EnsureResult>;
|
|
16
|
+
openBrowser?: (url: string) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const fail = (io: ProjectIo, message: string, fix: string): number => {
|
|
20
|
+
io.stderr(`✗ ${message}\nFix: ${fix}\n`);
|
|
21
|
+
return 1;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type Target = { kind: "none" } | { kind: "run"; id: string } | { kind: "error"; message: string; fix: string };
|
|
25
|
+
|
|
26
|
+
/** A run id, or a video named by its path or its title (as `vfrey videos` shows them). */
|
|
27
|
+
export function resolveTarget(arg: string | undefined, videos: VideoRow[], cwd: string, runIds: (id: string) => boolean): Target {
|
|
28
|
+
if (!arg) return { kind: "none" };
|
|
29
|
+
if (runIds(arg)) return { kind: "run", id: arg };
|
|
30
|
+
const asPath = path.resolve(cwd, arg);
|
|
31
|
+
const byPath = videos.find((v) => v.path === asPath || v.path === arg || path.basename(v.path) === arg || path.basename(v.path, ".mp4") === arg);
|
|
32
|
+
if (byPath) return { kind: "run", id: byPath.id };
|
|
33
|
+
const wanted = arg.toLowerCase();
|
|
34
|
+
const exact = videos.filter((v) => v.title?.toLowerCase() === wanted);
|
|
35
|
+
if (exact.length > 0) return { kind: "run", id: exact[0]!.id };
|
|
36
|
+
const partial = videos.filter((v) => v.title?.toLowerCase().includes(wanted) || path.basename(v.path).toLowerCase().includes(wanted));
|
|
37
|
+
if (partial.length === 1) return { kind: "run", id: partial[0]!.id };
|
|
38
|
+
if (partial.length > 1) {
|
|
39
|
+
return { kind: "error", message: `"${arg}" matches ${partial.length} videos: ${partial.slice(0, 5).map((v) => v.title ?? v.id).join("; ")}.`, fix: "pass the run id or the full path from `npx vfrey videos`." };
|
|
40
|
+
}
|
|
41
|
+
return { kind: "error", message: `No run or video matches "${arg}".`, fix: "run `npx vfrey videos` for the list, then pass a run id, a path, or a title." };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** `vfrey open [run|video] [--no-browser] [--json]`: reuse or start this desk's dashboard, open it, print the URL. */
|
|
45
|
+
export async function runOpen(argv: string[], io: OpenIo): Promise<number> {
|
|
46
|
+
const home = resolveHome(io.env, io.homedir ?? os.homedir(), io.cwd);
|
|
47
|
+
const arg = argv.find((a) => !a.startsWith("--"));
|
|
48
|
+
const notice = projectNotice(home);
|
|
49
|
+
const desk = deskOf(home);
|
|
50
|
+
const dbFile = path.join(home.dataDir, "pipeline.sqlite");
|
|
51
|
+
let videos: VideoRow[] = [];
|
|
52
|
+
const runIds = new Set<string>();
|
|
53
|
+
let hasRuns = false;
|
|
54
|
+
if (existsSync(dbFile)) {
|
|
55
|
+
const store = new Store(dbFile);
|
|
56
|
+
try {
|
|
57
|
+
videos = listVideos(store.db, home.dataDir);
|
|
58
|
+
for (const row of store.db.prepare(`SELECT id FROM jobs`).all() as Array<{ id: string }>) runIds.add(row.id);
|
|
59
|
+
hasRuns = store.db.prepare(`SELECT 1 FROM jobs WHERE kind = 'pipeline' LIMIT 1`).get() !== undefined;
|
|
60
|
+
} finally {
|
|
61
|
+
store.close();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Outside a project, with nothing on the machine desk, there is no way to know which project is meant. Do not guess.
|
|
65
|
+
if (home.source === "machine" && !hasRuns) {
|
|
66
|
+
const projects = readIndex(home.machineHome).filter((p) => !isMissing(p));
|
|
67
|
+
if (projects.length > 0) {
|
|
68
|
+
const list = projects.map((p) => ` ${p.name} ${p.root}`).join("\n");
|
|
69
|
+
io.stderr(`✗ Not in a vfrey project, and the machine desk has no videos. Projects on this machine:\n${list}\n`);
|
|
70
|
+
io.stderr("Fix: cd into one of them, then run `npx vfrey open`.\n");
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (notice) io.stderr(notice);
|
|
75
|
+
const target = resolveTarget(arg, videos, io.cwd, (id) => runIds.has(id));
|
|
76
|
+
if (target.kind === "error") return fail(io, target.message, target.fix);
|
|
77
|
+
const ensure = io.ensure ?? ensureDashboard;
|
|
78
|
+
const result = await ensure({ desk, env: io.env, cwd: io.cwd });
|
|
79
|
+
if (!result.ok) return fail(io, result.message, result.fix);
|
|
80
|
+
const url = target.kind === "run" ? `${result.origin}/runs/${target.id}` : `${result.origin}/`;
|
|
81
|
+
if (!browserDisabled(argv, io.env)) (io.openBrowser ?? openBrowser)(url);
|
|
82
|
+
if (argv.includes("--json")) {
|
|
83
|
+
io.stdout(JSON.stringify({ url, started: result.started, project: projectJson(home), runId: target.kind === "run" ? target.id : null }, null, 2) + "\n");
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
io.stdout(`${url}\n`);
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { agentSkillTarget } from "../skill-agents.js";
|
|
4
|
+
import { installSkillIfMissing, packageVersion } from "../skill-install.js";
|
|
5
|
+
import { hasMarker, newMarker, projectAt, readMarker, writeMarker, type Project } from "./marker.js";
|
|
6
|
+
import { CLAUDE_MD, GEMINI_MD, agentsMd, slug } from "./templates.js";
|
|
7
|
+
|
|
8
|
+
export const GITIGNORE_LINES = ["node_modules/", ".vfrey/", "videos/"];
|
|
9
|
+
|
|
10
|
+
export interface ScaffoldResult {
|
|
11
|
+
project: Project;
|
|
12
|
+
/** Paths (relative to the root) this call created. Empty when everything was already there. */
|
|
13
|
+
created: string[];
|
|
14
|
+
/** Paths that were already there and were left alone. */
|
|
15
|
+
kept: string[];
|
|
16
|
+
/** True when the folder was already a vfrey project and was only repaired. */
|
|
17
|
+
existing: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const PACKAGE_NAME = "@vfrey/vfrey";
|
|
21
|
+
|
|
22
|
+
/** The scripts a project's package.json gets: the commands a person or an agent types most. */
|
|
23
|
+
export const PROJECT_SCRIPTS: Record<string, string> = {
|
|
24
|
+
dashboard: "vfrey dashboard",
|
|
25
|
+
open: "vfrey open",
|
|
26
|
+
doctor: "vfrey doctor",
|
|
27
|
+
videos: "vfrey videos",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** package.json: private, depends on this exact package version, with the four scripts. An existing file only gains what is missing. */
|
|
31
|
+
function ensurePackageJson(root: string, name: string): "written" | "repaired" | "kept" {
|
|
32
|
+
const file = path.join(root, "package.json");
|
|
33
|
+
if (!existsSync(file)) {
|
|
34
|
+
const pkg = { name: slug(name), private: true, version: "0.0.0", scripts: PROJECT_SCRIPTS, dependencies: { [PACKAGE_NAME]: packageVersion() } };
|
|
35
|
+
writeFileSync(file, JSON.stringify(pkg, null, 2) + "\n");
|
|
36
|
+
return "written";
|
|
37
|
+
}
|
|
38
|
+
let pkg: { scripts?: Record<string, string>; dependencies?: Record<string, string> };
|
|
39
|
+
try {
|
|
40
|
+
pkg = JSON.parse(readFileSync(file, "utf8"));
|
|
41
|
+
if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) return "kept";
|
|
42
|
+
} catch {
|
|
43
|
+
return "kept";
|
|
44
|
+
}
|
|
45
|
+
let changed = false;
|
|
46
|
+
pkg.dependencies ??= {};
|
|
47
|
+
if (!pkg.dependencies[PACKAGE_NAME] && !(pkg as { devDependencies?: Record<string, string> }).devDependencies?.[PACKAGE_NAME]) {
|
|
48
|
+
pkg.dependencies[PACKAGE_NAME] = packageVersion();
|
|
49
|
+
changed = true;
|
|
50
|
+
}
|
|
51
|
+
pkg.scripts ??= {};
|
|
52
|
+
for (const [key, value] of Object.entries(PROJECT_SCRIPTS)) {
|
|
53
|
+
if (pkg.scripts[key] === undefined) {
|
|
54
|
+
pkg.scripts[key] = value;
|
|
55
|
+
changed = true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (changed) writeFileSync(file, JSON.stringify(pkg, null, 2) + "\n");
|
|
59
|
+
return changed ? "repaired" : "kept";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Writes the project layout into `root`: the marker, `.vfrey/`, `scenes/`, `scripts/`, `videos/`, `.gitignore`,
|
|
64
|
+
* package.json, AGENTS.md, CLAUDE.md, GEMINI.md and the skill for Claude Code (`.claude/skills/vfrey`) and for every other agent
|
|
65
|
+
* (`.agents/skills/vfrey`). Idempotent: an existing file is never overwritten and is reported as kept; a missing one is
|
|
66
|
+
* written. An existing `.gitignore` only gains missing lines, and an existing package.json only gains what is missing.
|
|
67
|
+
*/
|
|
68
|
+
export function scaffoldProject(root: string, options: { name?: string; engine?: string } = {}): ScaffoldResult {
|
|
69
|
+
const created: string[] = [];
|
|
70
|
+
const kept: string[] = [];
|
|
71
|
+
mkdirSync(root, { recursive: true });
|
|
72
|
+
const existing = hasMarker(root);
|
|
73
|
+
if (!existing) {
|
|
74
|
+
const marker = newMarker(options.name?.trim() || path.basename(path.resolve(root)));
|
|
75
|
+
writeMarker(root, options.engine ? ({ ...marker, engine: options.engine } as typeof marker) : marker);
|
|
76
|
+
created.push("vfrey.json");
|
|
77
|
+
}
|
|
78
|
+
for (const dir of [".vfrey", "scenes", "scripts", "videos"]) {
|
|
79
|
+
const full = path.join(root, dir);
|
|
80
|
+
if (!existsSync(full)) {
|
|
81
|
+
mkdirSync(full, { recursive: true });
|
|
82
|
+
created.push(`${dir}/`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const ignore = path.join(root, ".gitignore");
|
|
86
|
+
const have = existsSync(ignore) ? readFileSync(ignore, "utf8") : null;
|
|
87
|
+
const present = new Set((have ?? "").split(/\r?\n/).map((line) => line.trim()));
|
|
88
|
+
const missing = GITIGNORE_LINES.filter((line) => !present.has(line));
|
|
89
|
+
if (missing.length > 0) {
|
|
90
|
+
const prefix = have && have.length > 0 && !have.endsWith("\n") ? "\n" : "";
|
|
91
|
+
writeFileSync(ignore, (have ?? "") + prefix + missing.join("\n") + "\n");
|
|
92
|
+
created.push(".gitignore");
|
|
93
|
+
} else kept.push(".gitignore");
|
|
94
|
+
const project = projectAt(root);
|
|
95
|
+
const engine = options.engine ?? (readMarker(root) as { engine?: unknown }).engine;
|
|
96
|
+
const record = (rel: string, result: "written" | "repaired" | "kept") => (result === "kept" ? kept : created).push(rel);
|
|
97
|
+
const writeIfMissing = (rel: string, text: string) => {
|
|
98
|
+
const file = path.join(root, rel);
|
|
99
|
+
if (existsSync(file)) return record(rel, "kept");
|
|
100
|
+
writeFileSync(file, text);
|
|
101
|
+
record(rel, "written");
|
|
102
|
+
};
|
|
103
|
+
record("package.json", ensurePackageJson(root, project.name));
|
|
104
|
+
writeIfMissing("AGENTS.md", agentsMd(project.name, typeof engine === "string" ? engine : undefined));
|
|
105
|
+
writeIfMissing("CLAUDE.md", CLAUDE_MD);
|
|
106
|
+
writeIfMissing("GEMINI.md", GEMINI_MD);
|
|
107
|
+
for (const agent of ["claude", "codex"] as const) {
|
|
108
|
+
const dir = agentSkillTarget(agent, {}, root, true);
|
|
109
|
+
record(`${path.relative(root, dir).split(path.sep).join("/")}/`, installSkillIfMissing(dir));
|
|
110
|
+
}
|
|
111
|
+
return { project, created, kept, existing };
|
|
112
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** The text of the files `init` writes into a project. Short on purpose: an agent reads these on every session. */
|
|
2
|
+
|
|
3
|
+
export function agentsMd(name: string, engine?: string): string {
|
|
4
|
+
return `# ${name}: a vfrey project
|
|
5
|
+
|
|
6
|
+
This folder makes short videos. The person talks to you; you run \`npx vfrey\`. They do not need to know any command.
|
|
7
|
+
|
|
8
|
+
## Make a video
|
|
9
|
+
|
|
10
|
+
Use the vfrey skill (\`.claude/skills/vfrey\` or \`.agents/skills/vfrey\`) if you have it. If not, this is the flow:
|
|
11
|
+
|
|
12
|
+
1. Write the script yourself: \`scripts/<idea>.json\` (\`npx vfrey tick --script-template\` prints an example). Plain and accurate, 6 to 10 short lines, nothing invented.
|
|
13
|
+
2. Design a scene for the idea in \`scenes/<idea>/\` (\`npx vfrey scene init scenes/<idea>\`). No fixed templates: two ideas should not look alike. Preview with \`npx vfrey render --scene scenes/<idea> --engine ${engine ?? "<engine>"} --script scripts/<idea>.json --frame 2.5 out.png\`.
|
|
14
|
+
3. \`npx vfrey tick --now --about "<subject>" --script scripts/<idea>.json --scene scenes/<idea> --engine ${engine ?? "<engine>"}\`
|
|
15
|
+
4. Tell the person the path of the video in \`videos/\` and the \`See it:\` link.
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
- \`npx vfrey open [run|video]\`: show things. Opens this project's dashboard and prints its URL.
|
|
20
|
+
- \`npx vfrey videos\`: finished videos, newest first, with paths.
|
|
21
|
+
- \`npx vfrey projects\`: the other projects on this machine.
|
|
22
|
+
- \`npx vfrey doctor\`: check the machine. Run it first when something fails.
|
|
23
|
+
|
|
24
|
+
\`npx vfrey --help\` lists the rest.
|
|
25
|
+
|
|
26
|
+
## Where things go
|
|
27
|
+
|
|
28
|
+
\`scenes/\`, \`scripts/\`, \`videos/\` (finished videos, \`YYYY-MM-DD <title>.mp4\` with a transcript; do not rename or move them). \`.vfrey/\` is vfrey's data: do not edit it.
|
|
29
|
+
`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const CLAUDE_MD = "@AGENTS.md\n";
|
|
33
|
+
/** Gemini CLI reads GEMINI.md, not AGENTS.md; it supports @file imports. */
|
|
34
|
+
export const GEMINI_MD = "@AGENTS.md\n";
|
|
35
|
+
|
|
36
|
+
export function slug(name: string): string {
|
|
37
|
+
const cleaned = name
|
|
38
|
+
.toLowerCase()
|
|
39
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
40
|
+
.replace(/^[._-]+|[._-]+$/g, "");
|
|
41
|
+
return cleaned || "vfrey-project";
|
|
42
|
+
}
|