@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.
- 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 +361 -0
- package/src/api/cloud-files.ts +197 -0
- package/src/api/cloud-runs.ts +75 -0
- package/src/api/cloud.ts +413 -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 +273 -0
- package/src/cli.ts +468 -0
- package/src/cloud/bundle.ts +70 -0
- package/src/cloud/cloudrun-executor.ts +237 -0
- package/src/cloud/executor.ts +281 -0
- package/src/cloud/gcs-sign.ts +76 -0
- package/src/cloud/job.ts +106 -0
- package/src/cloud/remote-job.ts +67 -0
- package/src/cloud/render-job.ts +191 -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 +252 -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 +92 -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,361 @@
|
|
|
1
|
+
// The one vfrey API client. Every paid command uses it: it adds the token, maps failures to the CLI's ✗ and Fix: lines,
|
|
2
|
+
// and never prints the token.
|
|
3
|
+
import {
|
|
4
|
+
CLI_CLIENT_ID,
|
|
5
|
+
isApiError,
|
|
6
|
+
type ApiError,
|
|
7
|
+
type DeviceCodeResponse,
|
|
8
|
+
type DeviceTokenErrorCode,
|
|
9
|
+
type DeviceTokenResponse,
|
|
10
|
+
type MeResponse,
|
|
11
|
+
type NicheRequestBody,
|
|
12
|
+
type OrgsResponse,
|
|
13
|
+
type OrgSwitchResponse,
|
|
14
|
+
type NicheRequestResponse,
|
|
15
|
+
type NichesResponse,
|
|
16
|
+
type TopicsResponse,
|
|
17
|
+
type CloudConfirmResponse,
|
|
18
|
+
type CloudFileDecl,
|
|
19
|
+
type CloudFileView,
|
|
20
|
+
type CloudPlanResponse,
|
|
21
|
+
type CloudProjectView,
|
|
22
|
+
type CloudPullResponse,
|
|
23
|
+
type CloudRunBody,
|
|
24
|
+
type CreatorAlertsResponse,
|
|
25
|
+
type CreatorProfileView,
|
|
26
|
+
type CreatorSearchResponse,
|
|
27
|
+
type MailDraftBody,
|
|
28
|
+
type MailDraftPatch,
|
|
29
|
+
type MailDraftView,
|
|
30
|
+
type MailIdentityBody,
|
|
31
|
+
type MailIdentityView,
|
|
32
|
+
type WatchlistItemView,
|
|
33
|
+
type WatchlistView,
|
|
34
|
+
} from "./types.js";
|
|
35
|
+
|
|
36
|
+
export type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
37
|
+
|
|
38
|
+
/** A refusal the CLI prints as `✗ <message>` and `Fix: <fix>`. */
|
|
39
|
+
export class ApiFailure extends Error {
|
|
40
|
+
constructor(message: string, readonly fix: string, readonly status?: number, readonly code?: string) {
|
|
41
|
+
super(message);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ClientOptions {
|
|
46
|
+
baseUrl: string;
|
|
47
|
+
token?: string | null;
|
|
48
|
+
fetch?: FetchLike;
|
|
49
|
+
timeoutMs?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const TOKEN_SHAPE = /vfr_[A-Za-z0-9_-]+/g;
|
|
53
|
+
// eslint-disable-next-line no-control-regex
|
|
54
|
+
const CONTROL = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
55
|
+
|
|
56
|
+
/** Text from the server, safe to print: no control characters (no terminal escapes), no token. */
|
|
57
|
+
export function clean(text: unknown, token?: string | null): string {
|
|
58
|
+
let out = String(text ?? "").replace(CONTROL, " ").replace(TOKEN_SHAPE, "[token]");
|
|
59
|
+
if (token) out = out.split(token).join("[token]");
|
|
60
|
+
return out.trim();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const DEVICE_ERRORS = new Set<string>(["authorization_pending", "slow_down", "access_denied", "expired_token"]);
|
|
64
|
+
|
|
65
|
+
export class ApiClient {
|
|
66
|
+
private readonly fetchFn: FetchLike;
|
|
67
|
+
private readonly timeoutMs: number;
|
|
68
|
+
readonly host: string;
|
|
69
|
+
|
|
70
|
+
constructor(private readonly options: ClientOptions) {
|
|
71
|
+
this.fetchFn = options.fetch ?? ((input, init) => fetch(input, init));
|
|
72
|
+
this.timeoutMs = options.timeoutMs ?? 15_000;
|
|
73
|
+
this.host = new URL(options.baseUrl).host;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private async send(method: string, path: string, body?: unknown): Promise<{ status: number; json: unknown; text: string }> {
|
|
77
|
+
const headers: Record<string, string> = { accept: "application/json" };
|
|
78
|
+
if (this.options.token) headers.authorization = `Bearer ${this.options.token}`;
|
|
79
|
+
if (body !== undefined) headers["content-type"] = "application/json";
|
|
80
|
+
let response: Response;
|
|
81
|
+
try {
|
|
82
|
+
response = await this.fetchFn(`${this.options.baseUrl}${path}`, {
|
|
83
|
+
method,
|
|
84
|
+
headers,
|
|
85
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
86
|
+
redirect: "error",
|
|
87
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
88
|
+
});
|
|
89
|
+
} catch (error) {
|
|
90
|
+
const timedOut = error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
|
|
91
|
+
throw new ApiFailure(
|
|
92
|
+
timedOut ? `${this.host} did not answer in ${Math.round(this.timeoutMs / 1000)} seconds.` : `Could not reach ${this.host}.`,
|
|
93
|
+
"check your connection and try again. Free features work without vfrey accounts.",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
let text = "";
|
|
97
|
+
try {
|
|
98
|
+
text = await response.text();
|
|
99
|
+
} catch {
|
|
100
|
+
text = "";
|
|
101
|
+
}
|
|
102
|
+
let json: unknown = null;
|
|
103
|
+
try {
|
|
104
|
+
json = JSON.parse(text);
|
|
105
|
+
} catch {
|
|
106
|
+
json = null;
|
|
107
|
+
}
|
|
108
|
+
return { status: response.status, json, text };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Maps a non-2xx answer to the CLI's refusal. */
|
|
112
|
+
private fail(status: number, json: unknown, path = ""): ApiFailure {
|
|
113
|
+
const token = this.options.token;
|
|
114
|
+
const err = isApiError(json) ? (json as ApiError).error : null;
|
|
115
|
+
const message = err ? clean(err.message, token) : "";
|
|
116
|
+
if (status === 401) return new ApiFailure("You are not signed in, or this token was revoked.", "run `npx vfrey login`.", 401, "unauthorized");
|
|
117
|
+
if (status === 503 && err?.code === "capacity_paused") {
|
|
118
|
+
return new ApiFailure("vfrey is paused for new data right now.", "try again later. Cached results still work.", 503, "capacity_paused");
|
|
119
|
+
}
|
|
120
|
+
if (status === 402 && err?.code === "credits_required") {
|
|
121
|
+
const url = err.buyCreditsUrl ? clean(err.buyCreditsUrl, token) : "";
|
|
122
|
+
const need = typeof err.required === "number" ? ` It needs ${err.required} credits` : "";
|
|
123
|
+
const have = typeof err.balance === "number" ? `${need ? "; you have" : " You have"} ${err.balance}` : "";
|
|
124
|
+
const detail = need || have ? `${need}${have}.` : "";
|
|
125
|
+
return new ApiFailure(`${message || "Not enough credits."}${detail ? ` ${detail.trim()}` : ""}`, url ? `Buy credits: ${url}` : "buy credits on your vfrey billing page.", 402, "credits_required");
|
|
126
|
+
}
|
|
127
|
+
if (status === 402) {
|
|
128
|
+
const url = err?.upgradeUrl ? clean(err.upgradeUrl, token) : "";
|
|
129
|
+
return new ApiFailure(message || "This feature is not in your plan.", url ? `upgrade your plan at ${url}` : "upgrade your plan at your vfrey billing page.", 402, "plan_required");
|
|
130
|
+
}
|
|
131
|
+
if (status === 429 && err?.code === "rate_limited") {
|
|
132
|
+
return new ApiFailure(message || "Too many requests.", "wait a minute and try again.", 429, "rate_limited");
|
|
133
|
+
}
|
|
134
|
+
if (status === 429 && (err as { meter?: unknown } | null)?.meter === "storage") {
|
|
135
|
+
const url = err?.upgradeUrl ? clean(err.upgradeUrl, token) : "";
|
|
136
|
+
return new ApiFailure(message || "Your plan's cloud storage is full.", `free space (delete old videos from the cloud project) or ${url ? `upgrade at ${url}` : "upgrade your plan"}. Scenes and scripts are small; push without --videos.`, 429, "limit_reached");
|
|
137
|
+
}
|
|
138
|
+
if (status === 429) {
|
|
139
|
+
const resets = err?.resetsAt ? clean(err.resetsAt, token) : "";
|
|
140
|
+
const url = err?.upgradeUrl ? clean(err.upgradeUrl, token) : "";
|
|
141
|
+
const fix = resets ? `your limit resets at ${resets}${url ? `, or upgrade at ${url}` : ""}.` : "wait a little and try again.";
|
|
142
|
+
// The server names the meter when it can; otherwise the route says which one ran out.
|
|
143
|
+
const named = (err as { meter?: unknown } | null)?.meter;
|
|
144
|
+
const meter = typeof named === "string" && named ? clean(named, token)
|
|
145
|
+
: /^\/api\/v1\/(topics|niches)/.test(path) ? "topics.requests" : /^\/api\/v1\/creators\/(tiktok|instagram|youtube)\//.test(path) ? "creators.profiles" : "";
|
|
146
|
+
return new ApiFailure(`You reached this month's limit${meter ? ` for ${meter}` : ""}.`, fix, 429, err?.code ?? "limit_reached");
|
|
147
|
+
}
|
|
148
|
+
return new ApiFailure(message || `The vfrey server answered ${status}.`, status >= 500 ? "try again in a moment." : "run `npx vfrey doctor` and check the line above.", status, err?.code);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** A JSON request. Throws ApiFailure for network trouble and any non-2xx answer. */
|
|
152
|
+
async request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
|
153
|
+
const { status, json } = await this.send(method, path, body);
|
|
154
|
+
if (status < 200 || status >= 300) throw this.fail(status, json, path);
|
|
155
|
+
return json as T;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** A request whose answer is plain text (a CSV export). Same refusals as `request`. */
|
|
159
|
+
async requestText(method: string, path: string): Promise<string> {
|
|
160
|
+
const { status, json, text } = await this.send(method, path);
|
|
161
|
+
if (status < 200 || status >= 300) throw this.fail(status, json, path);
|
|
162
|
+
return text;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Creator search (spec 017). Reads the server's index; costs credits. */
|
|
166
|
+
creatorSearch(query: Record<string, string>): Promise<CreatorSearchResponse> {
|
|
167
|
+
return this.request<CreatorSearchResponse>("GET", `/api/v1/creators/search?${new URLSearchParams(query).toString()}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
creatorProfile(platform: string, handle: string): Promise<CreatorProfileView> {
|
|
171
|
+
return this.request<CreatorProfileView>("GET", `/api/v1/creators/${encodeURIComponent(platform)}/${encodeURIComponent(handle)}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
creatorAlerts(): Promise<CreatorAlertsResponse> {
|
|
175
|
+
return this.request<CreatorAlertsResponse>("GET", "/api/v1/creators/alerts");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
watchlists(): Promise<{ watchlists: WatchlistView[] }> {
|
|
179
|
+
return this.request("GET", "/api/v1/watchlists");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
createWatchlist(name: string): Promise<{ id: string; name: string }> {
|
|
183
|
+
return this.request("POST", "/api/v1/watchlists", { name });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
watchlistItems(id: string): Promise<{ watchlist: { id: string; name: string }; items: WatchlistItemView[] }> {
|
|
187
|
+
return this.request("GET", `/api/v1/watchlists/${encodeURIComponent(id)}/items`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
addWatch(id: string, body: { platform: string; handle: string }): Promise<{ ok: boolean; created: boolean }> {
|
|
191
|
+
return this.request("POST", `/api/v1/watchlists/${encodeURIComponent(id)}/items`, body);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
removeWatch(id: string, platform: string, handle: string): Promise<{ ok: boolean }> {
|
|
195
|
+
return this.request("DELETE", `/api/v1/watchlists/${encodeURIComponent(id)}/items?${new URLSearchParams({ platform, handle }).toString()}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
watchlistCsv(id: string): Promise<string> {
|
|
199
|
+
return this.requestText("GET", `/api/v1/watchlists/${encodeURIComponent(id)}/export.csv`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ---------- Mail (spec 018). Draft, approve and send are three separate calls; nothing here chains them. ----------
|
|
203
|
+
|
|
204
|
+
mailDraft(body: MailDraftBody): Promise<{ draft: MailDraftView }> {
|
|
205
|
+
return this.request("POST", "/api/v1/mail/drafts", body);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
mailDrafts(query: { status?: string; limit?: number } = {}): Promise<{ drafts: MailDraftView[] }> {
|
|
209
|
+
const params = new URLSearchParams();
|
|
210
|
+
if (query.status) params.set("status", query.status);
|
|
211
|
+
if (query.limit) params.set("limit", String(query.limit));
|
|
212
|
+
const qs = params.toString();
|
|
213
|
+
return this.request("GET", `/api/v1/mail/drafts${qs ? `?${qs}` : ""}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
mailGet(id: string): Promise<{ draft: MailDraftView }> {
|
|
217
|
+
return this.request("GET", `/api/v1/mail/drafts/${encodeURIComponent(id)}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
mailEdit(id: string, patch: MailDraftPatch): Promise<{ draft: MailDraftView }> {
|
|
221
|
+
return this.request("PATCH", `/api/v1/mail/drafts/${encodeURIComponent(id)}`, patch);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
mailApprove(id: string): Promise<{ draft: MailDraftView }> {
|
|
225
|
+
return this.request("POST", `/api/v1/mail/drafts/${encodeURIComponent(id)}/approve`, {});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
mailSend(id: string): Promise<{ draft: MailDraftView }> {
|
|
229
|
+
return this.request("POST", `/api/v1/mail/drafts/${encodeURIComponent(id)}/send`, {});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
mailDiscard(id: string): Promise<{ draft: MailDraftView }> {
|
|
233
|
+
return this.request("POST", `/api/v1/mail/drafts/${encodeURIComponent(id)}/discard`, {});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
mailIdentity(): Promise<{ identity: MailIdentityView | null }> {
|
|
237
|
+
return this.request("GET", "/api/v1/mail/identity");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
mailSaveIdentity(body: MailIdentityBody): Promise<{ identity: MailIdentityView; verificationEmailed: boolean }> {
|
|
241
|
+
return this.request("POST", "/api/v1/mail/identity", body);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Ranked topics for one niche. Costs credits on the server. */
|
|
245
|
+
topics(query: { niche: string; limit?: number; since?: string }): Promise<TopicsResponse> {
|
|
246
|
+
const params = new URLSearchParams({ niche: query.niche });
|
|
247
|
+
if (query.limit !== undefined) params.set("limit", String(query.limit));
|
|
248
|
+
if (query.since) params.set("since", query.since);
|
|
249
|
+
return this.request<TopicsResponse>("GET", `/api/v1/topics?${params.toString()}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
niches(): Promise<NichesResponse> {
|
|
253
|
+
return this.request<NichesResponse>("GET", "/api/v1/niches");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
requestNiche(body: NicheRequestBody): Promise<NicheRequestResponse> {
|
|
257
|
+
return this.request<NicheRequestResponse>("POST", "/api/v1/niches", body);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
me(): Promise<MeResponse> {
|
|
261
|
+
return this.request<MeResponse>("GET", "/api/v1/me");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** The orgs this user belongs to (spec 019 A). */
|
|
265
|
+
orgs(): Promise<OrgsResponse> {
|
|
266
|
+
return this.request<OrgsResponse>("GET", "/api/v1/orgs");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Trades this token for one bound to another org. The server revokes the token used for the call. */
|
|
270
|
+
switchOrg(orgId: string): Promise<OrgSwitchResponse> {
|
|
271
|
+
return this.request<OrgSwitchResponse>("POST", "/api/v1/orgs/switch", { orgId });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ---------- Hosted projects and sync (spec 019 B) ----------
|
|
275
|
+
|
|
276
|
+
cloudLink(body: { localProjectId: string; name: string }): Promise<{ project: CloudProjectView }> {
|
|
277
|
+
return this.request("POST", "/api/v1/cloud/projects", body);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
cloudFiles(projectId: string): Promise<{ files: CloudFileView[] }> {
|
|
281
|
+
return this.request("GET", `/api/v1/cloud/projects/${encodeURIComponent(projectId)}/files`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** The diff: which of these files are new or changed, and a signed upload URL for each. */
|
|
285
|
+
cloudPlan(projectId: string, files: CloudFileDecl[]): Promise<CloudPlanResponse> {
|
|
286
|
+
return this.request("POST", `/api/v1/cloud/projects/${encodeURIComponent(projectId)}/files`, { files });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
cloudConfirm(projectId: string, files: CloudFileDecl[]): Promise<CloudConfirmResponse> {
|
|
290
|
+
return this.request("POST", `/api/v1/cloud/projects/${encodeURIComponent(projectId)}/files/confirm`, { files });
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
cloudPushRuns(projectId: string, runs: CloudRunBody[]): Promise<{ runs: number }> {
|
|
294
|
+
return this.request("POST", `/api/v1/cloud/projects/${encodeURIComponent(projectId)}/runs`, { runs });
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
cloudPull(projectId: string, query: { since?: string | null; kinds?: string }): Promise<CloudPullResponse> {
|
|
298
|
+
const params = new URLSearchParams();
|
|
299
|
+
if (query.since) params.set("since", query.since);
|
|
300
|
+
if (query.kinds) params.set("kinds", query.kinds);
|
|
301
|
+
const qs = params.toString();
|
|
302
|
+
return this.request("GET", `/api/v1/cloud/projects/${encodeURIComponent(projectId)}/pull${qs ? `?${qs}` : ""}`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* One transfer to or from a signed storage URL. The vfrey token is NEVER sent here: the signed URL is the credential,
|
|
307
|
+
* and it may live on another host. https only (or this machine). Throws ApiFailure; no redirects.
|
|
308
|
+
*/
|
|
309
|
+
async transfer(method: "PUT" | "GET", url: string, init: { headers?: Record<string, string>; body?: BodyInit; length?: number; timeoutMs?: number } = {}): Promise<Response> {
|
|
310
|
+
let target: URL;
|
|
311
|
+
try {
|
|
312
|
+
target = new URL(url);
|
|
313
|
+
} catch {
|
|
314
|
+
throw new ApiFailure("The server sent a storage link that is not a web address.", "update vfrey with `npx vfrey update`, or try again.");
|
|
315
|
+
}
|
|
316
|
+
const loopback = target.hostname === "localhost" || target.hostname === "127.0.0.1" || target.hostname === "[::1]";
|
|
317
|
+
if (target.protocol !== "https:" && !(target.protocol === "http:" && loopback)) {
|
|
318
|
+
throw new ApiFailure(`The storage link (${target.host}) is not https, so vfrey will not use it.`, "check the vfrey server's storage settings.");
|
|
319
|
+
}
|
|
320
|
+
const headers: Record<string, string> = { ...(init.headers ?? {}) };
|
|
321
|
+
delete headers.authorization;
|
|
322
|
+
delete headers.Authorization;
|
|
323
|
+
if (init.length !== undefined) headers["content-length"] = String(init.length);
|
|
324
|
+
let response: Response;
|
|
325
|
+
try {
|
|
326
|
+
response = await this.fetchFn(target.href, {
|
|
327
|
+
method,
|
|
328
|
+
headers,
|
|
329
|
+
...(init.body !== undefined ? { body: init.body, duplex: "half" } : {}),
|
|
330
|
+
redirect: "error",
|
|
331
|
+
signal: AbortSignal.timeout(init.timeoutMs ?? 600_000),
|
|
332
|
+
} as RequestInit);
|
|
333
|
+
} catch {
|
|
334
|
+
throw new ApiFailure(`Could not reach the storage host ${target.host}.`, "check your connection and run the command again. Only what changed is sent.");
|
|
335
|
+
}
|
|
336
|
+
if (!response.ok) {
|
|
337
|
+
throw new ApiFailure(`Storage answered ${response.status} for a ${method === "PUT" ? "upload" : "download"}.`, response.status === 403 ? "the signed link expired; run the command again." : "run the command again. Only what changed is sent.", response.status);
|
|
338
|
+
}
|
|
339
|
+
return response;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Revokes the calling token. */
|
|
343
|
+
async revokeCurrentToken(): Promise<void> {
|
|
344
|
+
const { status, json } = await this.send("DELETE", "/api/v1/tokens/current");
|
|
345
|
+
if (status < 200 || status >= 300) throw this.fail(status, json);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
deviceCode(): Promise<DeviceCodeResponse> {
|
|
349
|
+
return this.request<DeviceCodeResponse>("POST", "/api/v1/device/code", { client_id: CLI_CLIENT_ID });
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** One poll. The four device errors come back as data (whatever status carries them); anything else throws. */
|
|
353
|
+
async deviceToken(deviceCode: string): Promise<DeviceTokenResponse> {
|
|
354
|
+
const { status, json } = await this.send("POST", "/api/v1/device/token", { device_code: deviceCode, client_id: CLI_CLIENT_ID });
|
|
355
|
+
const body = json as { error?: unknown; access_token?: unknown } | null;
|
|
356
|
+
if (body && typeof body.error === "string" && DEVICE_ERRORS.has(body.error)) return { error: body.error as DeviceTokenErrorCode };
|
|
357
|
+
if (status >= 200 && status < 300 && body && typeof body.access_token === "string") return json as DeviceTokenResponse;
|
|
358
|
+
if (status >= 200 && status < 300) throw new ApiFailure("The vfrey server sent an answer this version does not understand.", "update vfrey with `npx vfrey update`.", status);
|
|
359
|
+
throw this.fail(status, json);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// The local side of `vfrey cloud` (spec 019 B): which files sync, hashing, the sync state, and safe writes.
|
|
2
|
+
// The path rules here mirror the console's; the server checks them again, and pull checks what the server sends.
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { createReadStream, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
export type Kind = "scene" | "script" | "video" | "transcript" | "meta";
|
|
8
|
+
|
|
9
|
+
export const MAX_TEXT_BYTES = 5 * 1024 * 1024;
|
|
10
|
+
export const MAX_VIDEO_BYTES = 500 * 1024 * 1024;
|
|
11
|
+
/** What one push may change, and how many files one request names (the server's caps). */
|
|
12
|
+
export const MAX_PUSH_BYTES = 1024 * 1024 * 1024;
|
|
13
|
+
export const MAX_FILES_PER_REQUEST = 200;
|
|
14
|
+
|
|
15
|
+
const SCENE_EXT = new Set(["html", "htm", "css", "js", "mjs", "ts", "tsx", "jsx", "json", "svg", "png", "jpg", "jpeg", "webp", "gif", "woff", "woff2", "txt", "md"]);
|
|
16
|
+
const SCRIPT_EXT = new Set(["json", "txt", "md"]);
|
|
17
|
+
const VIDEO_EXT = new Set(["mp4", "webm"]);
|
|
18
|
+
const TRANSCRIPT_EXT = new Set(["txt", "srt", "vtt"]);
|
|
19
|
+
|
|
20
|
+
export type PathCheck = { ok: true; kind: Kind } | { ok: false; reason: string };
|
|
21
|
+
|
|
22
|
+
/** Only relative paths under scenes/, scripts/ and videos/: no "..", no absolute path, no hidden or conflict files. */
|
|
23
|
+
export function checkSyncPath(p: unknown): PathCheck {
|
|
24
|
+
if (typeof p !== "string" || p.length === 0) return { ok: false, reason: "empty path" };
|
|
25
|
+
if (p.length > 300) return { ok: false, reason: "path too long" };
|
|
26
|
+
// eslint-disable-next-line no-control-regex
|
|
27
|
+
if (/[\u0000-\u001f\u007f]/.test(p)) return { ok: false, reason: "control characters" };
|
|
28
|
+
if (p.includes("\\") || p.startsWith("/") || /^[A-Za-z]:/.test(p)) return { ok: false, reason: "not a relative path" };
|
|
29
|
+
const parts = p.split("/");
|
|
30
|
+
if (parts.some((x) => x === "" || x === "." || x === "..")) return { ok: false, reason: "empty, . or .. part" };
|
|
31
|
+
if (parts.some((x) => x.startsWith("."))) return { ok: false, reason: "hidden file" };
|
|
32
|
+
if (parts.some((x) => x.length > 120)) return { ok: false, reason: "name too long" };
|
|
33
|
+
if (p.includes(".conflict-")) return { ok: false, reason: "conflict copy" };
|
|
34
|
+
if (parts.length < 2) return { ok: false, reason: "outside scenes/, scripts/, videos/" };
|
|
35
|
+
const last = parts[parts.length - 1]!;
|
|
36
|
+
const ext = last.includes(".") ? last.split(".").pop()!.toLowerCase() : "";
|
|
37
|
+
if (parts[0] === "scenes") return SCENE_EXT.has(ext) ? { ok: true, kind: "scene" } : { ok: false, reason: `.${ext || "(none)"} is not a scene file type` };
|
|
38
|
+
if (parts[0] === "scripts") return SCRIPT_EXT.has(ext) ? { ok: true, kind: "script" } : { ok: false, reason: `.${ext || "(none)"} is not a script file type` };
|
|
39
|
+
if (parts[0] === "videos") {
|
|
40
|
+
if (VIDEO_EXT.has(ext)) return { ok: true, kind: "video" };
|
|
41
|
+
if (TRANSCRIPT_EXT.has(ext)) return { ok: true, kind: "transcript" };
|
|
42
|
+
if (ext === "json") return { ok: true, kind: "meta" };
|
|
43
|
+
return { ok: false, reason: `.${ext || "(none)"} is not a video or transcript file type` };
|
|
44
|
+
}
|
|
45
|
+
return { ok: false, reason: "outside scenes/, scripts/, videos/" };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const maxBytes = (kind: Kind): number => (kind === "video" ? MAX_VIDEO_BYTES : MAX_TEXT_BYTES);
|
|
49
|
+
|
|
50
|
+
export interface LocalFile {
|
|
51
|
+
path: string;
|
|
52
|
+
abs: string;
|
|
53
|
+
kind: Kind;
|
|
54
|
+
size: number;
|
|
55
|
+
sha256: string;
|
|
56
|
+
mtime: Date;
|
|
57
|
+
}
|
|
58
|
+
export interface Skipped {
|
|
59
|
+
path: string;
|
|
60
|
+
reason: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function sha256File(file: string): Promise<string> {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const hash = createHash("sha256");
|
|
66
|
+
createReadStream(file).on("data", (chunk) => hash.update(chunk)).on("error", reject).on("end", () => resolve(hash.digest("hex")));
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Walks scenes/ and scripts/ (and videos/ with `videos`). Symlinks are never followed; anything that cannot sync is listed, not hidden. */
|
|
71
|
+
export async function scanProject(root: string, opts: { videos: boolean }): Promise<{ files: LocalFile[]; skipped: Skipped[] }> {
|
|
72
|
+
const files: LocalFile[] = [];
|
|
73
|
+
const skipped: Skipped[] = [];
|
|
74
|
+
const roots = ["scenes", "scripts", ...(opts.videos ? ["videos"] : [])];
|
|
75
|
+
const walk = async (rel: string): Promise<void> => {
|
|
76
|
+
let entries;
|
|
77
|
+
try {
|
|
78
|
+
entries = readdirSync(path.join(root, rel), { withFileTypes: true });
|
|
79
|
+
} catch {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
entries.sort((a, b) => (a.name < b.name ? -1 : 1));
|
|
83
|
+
for (const entry of entries) {
|
|
84
|
+
const next = `${rel}/${entry.name}`;
|
|
85
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name.includes(".conflict-")) continue;
|
|
86
|
+
if (entry.isSymbolicLink()) {
|
|
87
|
+
skipped.push({ path: next, reason: "symbolic link" });
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (entry.isDirectory()) {
|
|
91
|
+
await walk(next);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (!entry.isFile()) continue;
|
|
95
|
+
const checked = checkSyncPath(next);
|
|
96
|
+
if (!checked.ok) {
|
|
97
|
+
skipped.push({ path: next, reason: checked.reason });
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const abs = path.join(root, next);
|
|
101
|
+
const st = statSync(abs);
|
|
102
|
+
if (st.size > maxBytes(checked.kind)) {
|
|
103
|
+
skipped.push({ path: next, reason: `${Math.round(st.size / 1024 / 1024)} MB is over the ${Math.round(maxBytes(checked.kind) / 1024 / 1024)} MB limit for a ${checked.kind}` });
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
files.push({ path: next, abs, kind: checked.kind, size: st.size, sha256: await sha256File(abs), mtime: st.mtime });
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
for (const r of roots) {
|
|
110
|
+
const top = path.join(root, r);
|
|
111
|
+
if (existsSync(top) && lstatSync(top).isDirectory()) await walk(r);
|
|
112
|
+
}
|
|
113
|
+
return { files, skipped };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------- sync state: what the cloud held the last time this machine pushed or pulled ----------
|
|
117
|
+
|
|
118
|
+
export interface SyncState {
|
|
119
|
+
version: 1;
|
|
120
|
+
projectId: string;
|
|
121
|
+
cursor: string | null;
|
|
122
|
+
/** path -> sha256 the cloud held when this machine last saw it. */
|
|
123
|
+
files: Record<string, string>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const stateFile = (root: string) => path.join(root, ".vfrey", "cloud-sync.json");
|
|
127
|
+
|
|
128
|
+
export function readState(root: string, projectId: string): SyncState {
|
|
129
|
+
const fresh: SyncState = { version: 1, projectId, cursor: null, files: {} };
|
|
130
|
+
try {
|
|
131
|
+
const parsed = JSON.parse(readFileSync(stateFile(root), "utf8")) as Partial<SyncState>;
|
|
132
|
+
if (parsed.version !== 1 || parsed.projectId !== projectId || typeof parsed.files !== "object" || parsed.files === null) return fresh;
|
|
133
|
+
return { version: 1, projectId, cursor: typeof parsed.cursor === "string" ? parsed.cursor : null, files: { ...parsed.files } };
|
|
134
|
+
} catch {
|
|
135
|
+
return fresh;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function writeState(root: string, state: SyncState): void {
|
|
140
|
+
mkdirSync(path.dirname(stateFile(root)), { recursive: true });
|
|
141
|
+
const tmp = `${stateFile(root)}.tmp`;
|
|
142
|
+
writeFileSync(tmp, JSON.stringify(state, null, 2) + "\n");
|
|
143
|
+
renameSync(tmp, stateFile(root));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---------- classification ----------
|
|
147
|
+
|
|
148
|
+
export type Verdict = "same" | "new" | "push" | "pull" | "conflict";
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* One file, three facts: its hash here, its hash in the cloud, and the hash the cloud had when we last synced.
|
|
152
|
+
* Changed only here -> push. Changed only in the cloud -> pull. Changed in both (or never synced and different) -> conflict.
|
|
153
|
+
*/
|
|
154
|
+
export function classify(local: string | undefined, cloud: string | undefined, base: string | undefined): Verdict {
|
|
155
|
+
if (local !== undefined && cloud === undefined) return "new";
|
|
156
|
+
if (local === undefined) return "pull";
|
|
157
|
+
if (local === cloud) return "same";
|
|
158
|
+
if (base === cloud) return "push";
|
|
159
|
+
if (base === local) return "pull";
|
|
160
|
+
return "conflict";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** `<file>.conflict-<ISO time>`, with colons and dots made safe for every filesystem. A clash gets a counter. */
|
|
164
|
+
export function conflictPath(abs: string, at: Date): string {
|
|
165
|
+
const stamp = at.toISOString().replace(/[:.]/g, "-");
|
|
166
|
+
let candidate = `${abs}.conflict-${stamp}`;
|
|
167
|
+
for (let n = 2; existsSync(candidate); n += 1) candidate = `${abs}.conflict-${stamp}-${n}`;
|
|
168
|
+
return candidate;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Where a path from the server lands, or an error. The path must pass the sync rules, stay inside the project root, and
|
|
173
|
+
* no part of it that exists may be a symbolic link (a link could send a write outside the project).
|
|
174
|
+
*/
|
|
175
|
+
export function safeTarget(root: string, rel: string): { ok: true; abs: string } | { ok: false; reason: string } {
|
|
176
|
+
const checked = checkSyncPath(rel);
|
|
177
|
+
if (!checked.ok) return { ok: false, reason: checked.reason };
|
|
178
|
+
let cur = root;
|
|
179
|
+
for (const part of rel.split("/")) {
|
|
180
|
+
cur = path.join(cur, part);
|
|
181
|
+
try {
|
|
182
|
+
if (lstatSync(cur).isSymbolicLink()) return { ok: false, reason: "a part of the path is a symbolic link" };
|
|
183
|
+
} catch {
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const abs = path.join(root, ...rel.split("/"));
|
|
188
|
+
if (path.relative(root, abs).startsWith("..") || path.isAbsolute(path.relative(root, abs))) return { ok: false, reason: "outside the project" };
|
|
189
|
+
return { ok: true, abs };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function formatSize(n: number): string {
|
|
193
|
+
if (n >= 1024 ** 3) return `${(n / 1024 ** 3).toFixed(1)} GB`;
|
|
194
|
+
if (n >= 1024 ** 2) return `${(n / 1024 ** 2).toFixed(1)} MB`;
|
|
195
|
+
if (n >= 1024) return `${Math.round(n / 1024)} KB`;
|
|
196
|
+
return `${n} B`;
|
|
197
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Run metadata for `vfrey cloud push`: stages, checks and a publications summary from the project database.
|
|
2
|
+
// Only summaries leave the machine: no script text, no tokens, no account secrets.
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { DatabaseSync } from "node:sqlite";
|
|
6
|
+
import { storedVideoPath } from "../project/videos.js";
|
|
7
|
+
import type { CloudRunBody } from "./types.js";
|
|
8
|
+
|
|
9
|
+
const MAX_RUNS = 100;
|
|
10
|
+
|
|
11
|
+
type Row = { id: string; stage: string; status: string; payload: string; created_at: string; finished_at?: string | null };
|
|
12
|
+
type Payload = {
|
|
13
|
+
content?: { title?: unknown; outputPath?: unknown } | null;
|
|
14
|
+
rendered?: { durationSec?: unknown } | null;
|
|
15
|
+
checks?: unknown;
|
|
16
|
+
stages?: unknown;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const str = (v: unknown, max: number): string | null => (typeof v === "string" && v.trim() ? v.trim().slice(0, max) : null);
|
|
20
|
+
|
|
21
|
+
/** The newest pipeline runs in `<dataDir>/pipeline.sqlite`. `videoPaths` are the project-relative videos that are (or just were) in the cloud: only those are named. */
|
|
22
|
+
export function collectRuns(dataDir: string, root: string, videoPaths: ReadonlySet<string>): CloudRunBody[] {
|
|
23
|
+
const file = path.join(dataDir, "pipeline.sqlite");
|
|
24
|
+
if (!existsSync(file)) return [];
|
|
25
|
+
const db = new DatabaseSync(file, { readOnly: true });
|
|
26
|
+
try {
|
|
27
|
+
const rows = db.prepare(`SELECT * FROM jobs WHERE kind = 'pipeline' ORDER BY created_at DESC, id DESC LIMIT ${MAX_RUNS}`).all() as unknown as Row[];
|
|
28
|
+
const pubs = db.prepare(`SELECT platform, external_id, published_at, title FROM publications WHERE job_id = ? ORDER BY published_at`);
|
|
29
|
+
return rows.map((row) => {
|
|
30
|
+
let payload: Payload = {};
|
|
31
|
+
try {
|
|
32
|
+
payload = JSON.parse(row.payload) as Payload;
|
|
33
|
+
} catch {
|
|
34
|
+
payload = {};
|
|
35
|
+
}
|
|
36
|
+
const stored = payload.content?.outputPath;
|
|
37
|
+
let videoPath: string | null = null;
|
|
38
|
+
if (typeof stored === "string" && stored.endsWith(".mp4")) {
|
|
39
|
+
const rel = path.relative(root, storedVideoPath(dataDir, stored)).split(path.sep).join("/");
|
|
40
|
+
if (videoPaths.has(rel)) videoPath = rel;
|
|
41
|
+
}
|
|
42
|
+
const checks = (Array.isArray(payload.checks) ? payload.checks : []) as Array<Record<string, unknown>>;
|
|
43
|
+
const stages = Array.isArray(payload.stages) ? (payload.stages as Array<Record<string, unknown>>) : [{ stage: row.stage, status: row.status }];
|
|
44
|
+
const length = payload.rendered?.durationSec;
|
|
45
|
+
return {
|
|
46
|
+
localRunId: row.id,
|
|
47
|
+
title: str(payload.content?.title, 200),
|
|
48
|
+
status: row.status,
|
|
49
|
+
stage: row.stage,
|
|
50
|
+
startedAt: row.created_at,
|
|
51
|
+
finishedAt: row.finished_at ?? null,
|
|
52
|
+
durationSec: typeof length === "number" && Number.isFinite(length) ? length : null,
|
|
53
|
+
videoPath,
|
|
54
|
+
stages: stages.slice(0, 20).map((s) => ({ stage: str(s.stage, 60) ?? "", status: str(s.status, 40) ?? "", ...(str(s.at, 40) ? { at: str(s.at, 40)! } : {}) })),
|
|
55
|
+
checks: checks.slice(0, 100).map((c) => ({
|
|
56
|
+
id: str(c.id, 80) ?? "",
|
|
57
|
+
...(str(c.phase, 20) ? { phase: str(c.phase, 20)! } : {}),
|
|
58
|
+
...(str(c.severity, 20) ? { severity: str(c.severity, 20)! } : {}),
|
|
59
|
+
status: str(c.status, 20) ?? "",
|
|
60
|
+
...(str(c.message, 300) ? { message: str(c.message, 300)! } : {}),
|
|
61
|
+
fix: str(c.fix, 300),
|
|
62
|
+
...(typeof c.attempt === "number" ? { attempt: c.attempt } : {}),
|
|
63
|
+
})),
|
|
64
|
+
publications: (pubs.all(row.id) as Array<{ platform: string; external_id: string | null; published_at: string | null; title: string | null }>).slice(0, 20).map((p) => ({
|
|
65
|
+
platform: p.platform,
|
|
66
|
+
externalId: p.external_id,
|
|
67
|
+
publishedAt: p.published_at,
|
|
68
|
+
title: str(p.title, 200),
|
|
69
|
+
})),
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
} finally {
|
|
73
|
+
db.close();
|
|
74
|
+
}
|
|
75
|
+
}
|