@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,149 @@
|
|
|
1
|
+
// Read-only data for the Publications, Sources, Engage and Trends screens (spec 015 B).
|
|
2
|
+
import { redact } from "../queue/seal.js";
|
|
3
|
+
import type { Store } from "../queue/store.js";
|
|
4
|
+
import { summarizeGains, viewsGained } from "./ui/metrics-math.js";
|
|
5
|
+
|
|
6
|
+
type Row = Record<string, unknown>;
|
|
7
|
+
const str = (v: unknown): string => (typeof v === "string" ? v : "");
|
|
8
|
+
const strOrNull = (v: unknown): string | null => (typeof v === "string" ? v : null);
|
|
9
|
+
const numOrNull = (v: unknown): number | null => (typeof v === "number" && Number.isFinite(v) ? v : null);
|
|
10
|
+
const PLATFORMS = ["youtube", "tiktok", "instagram", "facebook"];
|
|
11
|
+
const ID = /^[A-Za-z0-9._-]{1,64}$/;
|
|
12
|
+
|
|
13
|
+
export const isDry = (externalId: string | null): boolean => !externalId || externalId === "dry-run";
|
|
14
|
+
|
|
15
|
+
/** A public link to the post, only where the platform's id alone is enough to build one. */
|
|
16
|
+
export function externalUrl(platform: string, externalId: string | null): string | null {
|
|
17
|
+
if (isDry(externalId) || !externalId || !/^[A-Za-z0-9_-]{3,64}$/.test(externalId)) return null;
|
|
18
|
+
if (platform === "youtube") return `https://www.youtube.com/watch?v=${externalId}`;
|
|
19
|
+
if (platform === "facebook") return `https://www.facebook.com/watch/?v=${externalId}`;
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const filterOf = (params: URLSearchParams, name: string, ok: (v: string) => boolean): string | null => {
|
|
24
|
+
const v = params.get(name);
|
|
25
|
+
return v && ok(v) ? v : null;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function seriesIds(store: Store): string[] {
|
|
29
|
+
return (store.db.prepare(`SELECT id FROM desk_series ORDER BY id`).all() as Row[]).map((r) => str(r.id));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function publicationsView(store: Store, params: URLSearchParams) {
|
|
33
|
+
const series = filterOf(params, "series", (v) => ID.test(v));
|
|
34
|
+
const platform = filterOf(params, "platform", (v) => PLATFORMS.includes(v));
|
|
35
|
+
const rows = store.db
|
|
36
|
+
.prepare(`SELECT * FROM publications WHERE (? IS NULL OR account_id = ?) AND (? IS NULL OR platform = ?) ORDER BY published_at DESC, rowid DESC LIMIT 500`)
|
|
37
|
+
.all(series, series, platform, platform) as Row[];
|
|
38
|
+
return {
|
|
39
|
+
ok: true,
|
|
40
|
+
series: seriesIds(store),
|
|
41
|
+
platforms: PLATFORMS,
|
|
42
|
+
publications: rows.map((row) => {
|
|
43
|
+
let proof: Row = {};
|
|
44
|
+
try {
|
|
45
|
+
proof = JSON.parse(str(row.disclosure)) as Row;
|
|
46
|
+
} catch {
|
|
47
|
+
proof = {};
|
|
48
|
+
}
|
|
49
|
+
let audit: Row = {};
|
|
50
|
+
try {
|
|
51
|
+
audit = JSON.parse(str(row.audit)) as Row;
|
|
52
|
+
} catch {
|
|
53
|
+
audit = {};
|
|
54
|
+
}
|
|
55
|
+
const externalId = strOrNull(row.external_id);
|
|
56
|
+
const dryRun = isDry(externalId) || audit.dryRun === true;
|
|
57
|
+
return {
|
|
58
|
+
id: str(row.id),
|
|
59
|
+
jobId: str(row.job_id),
|
|
60
|
+
seriesId: str(row.account_id),
|
|
61
|
+
platform: str(row.platform),
|
|
62
|
+
title: redact(str(row.title)),
|
|
63
|
+
status: dryRun ? "dry-run" : "published",
|
|
64
|
+
dryRun,
|
|
65
|
+
externalId: dryRun ? null : externalId,
|
|
66
|
+
externalUrl: dryRun ? null : externalUrl(str(row.platform), externalId),
|
|
67
|
+
// The proof the publish stage stored: which request field carried the disclosure, and the value it held.
|
|
68
|
+
disclosure: { field: strOrNull(proof.field), value: typeof proof.value === "boolean" ? proof.value : null, location: strOrNull(proof.location) },
|
|
69
|
+
// A dry run builds the request but never sends it.
|
|
70
|
+
disclosureSent: !dryRun,
|
|
71
|
+
publishedAt: str(row.published_at),
|
|
72
|
+
};
|
|
73
|
+
}),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function sourcesView(store: Store) {
|
|
78
|
+
const series = seriesIds(store);
|
|
79
|
+
const sources = (store.db.prepare(`SELECT * FROM desk_sources ORDER BY series_id, rowid`).all() as Row[]).map((row) => ({
|
|
80
|
+
id: str(row.id),
|
|
81
|
+
seriesId: str(row.series_id),
|
|
82
|
+
kind: str(row.kind),
|
|
83
|
+
value: redact(str(row.value)),
|
|
84
|
+
lastStatus: strOrNull(row.last_status),
|
|
85
|
+
lastFetchAt: strOrNull(row.last_fetch_at),
|
|
86
|
+
lastCount: numOrNull(row.last_count),
|
|
87
|
+
lastCode: strOrNull(row.last_code),
|
|
88
|
+
}));
|
|
89
|
+
const warnings = (store.db.prepare(`SELECT * FROM desk_warnings WHERE code = 'topics_unavailable' AND acked_at IS NULL ORDER BY created_at DESC LIMIT 20`).all() as Row[]).map((row) => ({
|
|
90
|
+
id: str(row.id),
|
|
91
|
+
seriesId: strOrNull(row.account_id),
|
|
92
|
+
code: str(row.code),
|
|
93
|
+
message: redact(str(row.message)),
|
|
94
|
+
createdAt: str(row.created_at),
|
|
95
|
+
}));
|
|
96
|
+
return { ok: true, series, sources, warnings };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function engageView(store: Store, params: URLSearchParams) {
|
|
100
|
+
const series = filterOf(params, "series", (v) => ID.test(v));
|
|
101
|
+
const rows = store.db
|
|
102
|
+
.prepare(`SELECT * FROM comments_handled WHERE platform != 'tiktok' AND (? IS NULL OR account_id = ?) ORDER BY created_at DESC, rowid DESC LIMIT 300`)
|
|
103
|
+
.all(series, series) as Row[];
|
|
104
|
+
return {
|
|
105
|
+
ok: true,
|
|
106
|
+
series: seriesIds(store),
|
|
107
|
+
// TikTok's API has no comment reply, so no TikTok rows exist and none are shown.
|
|
108
|
+
note: "TikTok is not listed: TikTok's API does not support replying to comments.",
|
|
109
|
+
replies: rows.map((row) => ({
|
|
110
|
+
platform: str(row.platform),
|
|
111
|
+
commentId: str(row.comment_id),
|
|
112
|
+
seriesId: str(row.account_id),
|
|
113
|
+
comment: row.comment_text ? redact(str(row.comment_text)) : null,
|
|
114
|
+
reply: row.reply_text ? redact(str(row.reply_text)) : null,
|
|
115
|
+
status: str(row.action) === "replied" && Number(row.dry_run) === 1 ? "drafted" : str(row.action),
|
|
116
|
+
code: strOrNull(row.code),
|
|
117
|
+
tone: strOrNull(row.tone),
|
|
118
|
+
at: str(row.created_at),
|
|
119
|
+
})),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function trendsView(store: Store) {
|
|
124
|
+
const live = (store.db.prepare(`SELECT account_id, platform, external_id FROM publications WHERE external_id IS NOT NULL AND external_id != 'dry-run'`).all() as Row[]).map((r) => ({ seriesId: str(r.account_id), platform: str(r.platform), externalId: str(r.external_id) }));
|
|
125
|
+
const snapshots = new Map<string, Array<{ capturedAt: string; views: number | null }>>();
|
|
126
|
+
for (const row of store.db.prepare(`SELECT account_id, platform, external_id, captured_at, views FROM metrics WHERE external_id != 'dry-run' ORDER BY captured_at DESC, rowid DESC`).all() as Row[]) {
|
|
127
|
+
const key = `${str(row.account_id)}\u0000${str(row.platform)}\u0000${str(row.external_id)}`;
|
|
128
|
+
const list = snapshots.get(key) ?? [];
|
|
129
|
+
if (list.length < 2) list.push({ capturedAt: str(row.captured_at), views: numOrNull(row.views) });
|
|
130
|
+
snapshots.set(key, list);
|
|
131
|
+
}
|
|
132
|
+
// Every live post counts once, measured or not, so "N of M" never hides a post that has no reading.
|
|
133
|
+
const posts = new Map<string, { seriesId: string; platform: string; externalId: string }>();
|
|
134
|
+
for (const p of live) posts.set(`${p.seriesId}\u0000${p.platform}\u0000${p.externalId}`, p);
|
|
135
|
+
for (const key of snapshots.keys()) {
|
|
136
|
+
const [seriesId = "", platform = "", externalId = ""] = key.split("\u0000");
|
|
137
|
+
if (!posts.has(key)) posts.set(key, { seriesId, platform, externalId });
|
|
138
|
+
}
|
|
139
|
+
const groups = new Map<string, { seriesId: string; platform: string; posts: Array<{ externalId: string; gained: number | null; from: string | null; to: string | null; reason: string }> }>();
|
|
140
|
+
for (const [key, p] of posts) {
|
|
141
|
+
const g = groups.get(`${p.seriesId}\u0000${p.platform}`) ?? { seriesId: p.seriesId, platform: p.platform, posts: [] };
|
|
142
|
+
g.posts.push({ externalId: p.externalId, ...viewsGained(snapshots.get(key)) });
|
|
143
|
+
groups.set(`${p.seriesId}\u0000${p.platform}`, g);
|
|
144
|
+
}
|
|
145
|
+
const trends = [...groups.values()]
|
|
146
|
+
.sort((a, b) => a.seriesId.localeCompare(b.seriesId) || a.platform.localeCompare(b.platform))
|
|
147
|
+
.map((g) => ({ seriesId: g.seriesId, platform: g.platform, ...summarizeGains(g.posts as never), posts: g.posts }));
|
|
148
|
+
return { ok: true, trends };
|
|
149
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { credentialsKey, decryptSecret } from "@vfrey/contracts";
|
|
2
|
+
import type { Account, AppConfig } from "@vfrey/contracts";
|
|
3
|
+
import type { Store } from "../queue/store.js";
|
|
4
|
+
|
|
5
|
+
export type SecretOwnerKind = "account" | "series";
|
|
6
|
+
|
|
7
|
+
/** Console columns, per owner. */
|
|
8
|
+
export const SECRET_FIELDS: Record<SecretOwnerKind, string[]> = {
|
|
9
|
+
account: [
|
|
10
|
+
"youtubeApiKey",
|
|
11
|
+
"redditClientId",
|
|
12
|
+
"redditClientSecret",
|
|
13
|
+
"tiktokClientKey",
|
|
14
|
+
"tiktokClientSecret",
|
|
15
|
+
"metaAppId",
|
|
16
|
+
"metaAppSecret",
|
|
17
|
+
],
|
|
18
|
+
series: [
|
|
19
|
+
"googleClientId",
|
|
20
|
+
"googleClientSecret",
|
|
21
|
+
"youtubeRefreshToken",
|
|
22
|
+
"tiktokRefreshToken",
|
|
23
|
+
"tiktokOpenId",
|
|
24
|
+
"igUserId",
|
|
25
|
+
"igAccessToken",
|
|
26
|
+
"fbPageId",
|
|
27
|
+
"fbPageToken",
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Account fields land in fixed env names. */
|
|
32
|
+
export const ACCOUNT_SECRET_ENV: Record<string, string> = {
|
|
33
|
+
youtubeApiKey: "YOUTUBE_API_KEY",
|
|
34
|
+
redditClientId: "REDDIT_CLIENT_ID",
|
|
35
|
+
redditClientSecret: "REDDIT_CLIENT_SECRET",
|
|
36
|
+
tiktokClientKey: "TIKTOK_CLIENT_KEY",
|
|
37
|
+
tiktokClientSecret: "TIKTOK_CLIENT_SECRET",
|
|
38
|
+
metaAppId: "META_APP_ID",
|
|
39
|
+
metaAppSecret: "META_APP_SECRET",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Series fields land in the env names the series' platform config names. */
|
|
43
|
+
function seriesSecretEnv(account: Account, field: string): string | null {
|
|
44
|
+
const { youtube, tiktok, instagram, facebook } = account.platforms;
|
|
45
|
+
switch (field) {
|
|
46
|
+
case "googleClientId":
|
|
47
|
+
return youtube?.clientIdEnv ?? null;
|
|
48
|
+
case "googleClientSecret":
|
|
49
|
+
return youtube?.clientSecretEnv ?? null;
|
|
50
|
+
case "youtubeRefreshToken":
|
|
51
|
+
return youtube?.refreshTokenEnv ?? null;
|
|
52
|
+
case "tiktokRefreshToken":
|
|
53
|
+
return tiktok?.refreshTokenEnv ?? null;
|
|
54
|
+
case "tiktokOpenId":
|
|
55
|
+
return tiktok?.openIdEnv ?? null;
|
|
56
|
+
case "igUserId":
|
|
57
|
+
return instagram?.igUserIdEnv ?? null;
|
|
58
|
+
case "igAccessToken":
|
|
59
|
+
return instagram?.accessTokenEnv ?? null;
|
|
60
|
+
case "fbPageId":
|
|
61
|
+
return facebook?.pageIdEnv ?? null;
|
|
62
|
+
case "fbPageToken":
|
|
63
|
+
return facebook?.pageTokenEnv ?? null;
|
|
64
|
+
default:
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function secretFieldError(ownerKind: string, field: string): { refusal: string; fix: string } | null {
|
|
70
|
+
if (ownerKind !== "account" && ownerKind !== "series") {
|
|
71
|
+
return { refusal: `Secret owner ${ownerKind} is not account or series.`, fix: "Use --account <id> or --series <id>." };
|
|
72
|
+
}
|
|
73
|
+
if (SECRET_FIELDS[ownerKind].includes(field)) return null;
|
|
74
|
+
return {
|
|
75
|
+
refusal: `${field} is not a ${ownerKind} secret field.`,
|
|
76
|
+
fix: `Use one of: ${SECRET_FIELDS[ownerKind].join(", ")}.`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function warnUnreadable(store: Store, accountId: string, ownerKind: string, field: string): void {
|
|
81
|
+
const message = `The ${ownerKind} secret ${field} could not be decrypted. Set it again with the current CREDENTIALS_KEY.`;
|
|
82
|
+
const existing = store.db
|
|
83
|
+
.prepare(`SELECT id FROM desk_warnings WHERE code = 'secret_unreadable' AND message = ? AND acked_at IS NULL LIMIT 1`)
|
|
84
|
+
.get(message);
|
|
85
|
+
if (existing) return;
|
|
86
|
+
store.insertWarning({ accountId, jobId: null, code: "secret_unreadable", message });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Decrypts the desk secrets of one series and its account into env names.
|
|
91
|
+
* The result is for one job's runtime env. Nothing here touches process.env.
|
|
92
|
+
*/
|
|
93
|
+
export function secretEnvFor(store: Store, account: Account, rawKey: string | undefined): Record<string, string> {
|
|
94
|
+
const rows = store.db
|
|
95
|
+
.prepare(
|
|
96
|
+
`SELECT owner_kind, field, ciphertext FROM desk_secrets
|
|
97
|
+
WHERE (owner_kind = 'series' AND owner_id = ?)
|
|
98
|
+
OR (owner_kind = 'account' AND owner_id = (SELECT account_id FROM desk_series WHERE id = ?))`,
|
|
99
|
+
)
|
|
100
|
+
.all(account.id, account.id) as Array<{ owner_kind: string; field: string; ciphertext: string }>;
|
|
101
|
+
if (rows.length === 0) return {};
|
|
102
|
+
let key: Buffer | null = null;
|
|
103
|
+
try {
|
|
104
|
+
key = credentialsKey(rawKey ?? "");
|
|
105
|
+
} catch {
|
|
106
|
+
key = null;
|
|
107
|
+
}
|
|
108
|
+
const env: Record<string, string> = {};
|
|
109
|
+
for (const row of rows) {
|
|
110
|
+
const name = row.owner_kind === "account" ? ACCOUNT_SECRET_ENV[row.field] : seriesSecretEnv(account, row.field);
|
|
111
|
+
if (!name) continue;
|
|
112
|
+
try {
|
|
113
|
+
if (!key) throw new Error("no key");
|
|
114
|
+
env[name] = decryptSecret(row.ciphertext, key);
|
|
115
|
+
} catch {
|
|
116
|
+
warnUnreadable(store, account.id, row.owner_kind, row.field);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return env;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Recomputes the config fields that loadConfig derives from env when a desk secret supplies them. */
|
|
123
|
+
export function configWithSecrets(config: AppConfig, env: Record<string, string>): AppConfig {
|
|
124
|
+
return {
|
|
125
|
+
...config,
|
|
126
|
+
googleApiKey: env.YOUTUBE_API_KEY ?? config.googleApiKey,
|
|
127
|
+
reddit: {
|
|
128
|
+
...config.reddit,
|
|
129
|
+
clientId: env.REDDIT_CLIENT_ID ?? config.reddit.clientId,
|
|
130
|
+
clientSecret: env.REDDIT_CLIENT_SECRET ?? config.reddit.clientSecret,
|
|
131
|
+
},
|
|
132
|
+
tiktok: {
|
|
133
|
+
...config.tiktok,
|
|
134
|
+
clientKey: env.TIKTOK_CLIENT_KEY ?? config.tiktok.clientKey,
|
|
135
|
+
clientSecret: env.TIKTOK_CLIENT_SECRET ?? config.tiktok.clientSecret,
|
|
136
|
+
},
|
|
137
|
+
meta: {
|
|
138
|
+
...config.meta,
|
|
139
|
+
appId: env.META_APP_ID ?? config.meta.appId,
|
|
140
|
+
appSecret: env.META_APP_SECRET ?? config.meta.appSecret,
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import type { Account } from "@vfrey/contracts";
|
|
4
|
+
import { parseAccounts } from "../config.js";
|
|
5
|
+
import type { Store } from "../queue/store.js";
|
|
6
|
+
import { applyDeskAccount } from "./overlay.js";
|
|
7
|
+
|
|
8
|
+
const FIRST_RUN_FILE = fileURLToPath(new URL("../../accounts.first-run.json", import.meta.url));
|
|
9
|
+
|
|
10
|
+
/** Non-secret platform settings that live in desk rows. Env-variable names stay in base_json. */
|
|
11
|
+
const PLATFORM_FIELDS: Record<string, string[]> = {
|
|
12
|
+
youtube: ["privacyStatus", "categoryId", "madeForKids"],
|
|
13
|
+
tiktok: ["privacyLevel"],
|
|
14
|
+
instagram: ["shareToFeed"],
|
|
15
|
+
facebook: [],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const SOURCE_KINDS = ["rss", "subreddit", "youtubeChannel", "youtubeTrending"] as const;
|
|
19
|
+
|
|
20
|
+
export function firstRunTemplate(): Account {
|
|
21
|
+
const [account] = parseAccounts(JSON.parse(readFileSync(FIRST_RUN_FILE, "utf8")) as unknown);
|
|
22
|
+
if (!account) throw new Error("accounts.first-run.json has no account.");
|
|
23
|
+
return account;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Imports each account that has no desk series yet. Returns the ids it added. */
|
|
27
|
+
export function importAccounts(store: Store, accounts: Account[]): string[] {
|
|
28
|
+
const added: string[] = [];
|
|
29
|
+
for (const account of accounts) {
|
|
30
|
+
const exists = store.db.prepare(`SELECT id FROM desk_series WHERE id = ?`).get(account.id);
|
|
31
|
+
if (exists) continue;
|
|
32
|
+
store.db.exec("BEGIN");
|
|
33
|
+
try {
|
|
34
|
+
store.db
|
|
35
|
+
.prepare(`INSERT OR IGNORE INTO desk_accounts (id, name, owner_id, paused) VALUES (?, ?, NULL, 0)`)
|
|
36
|
+
.run(account.id, account.id);
|
|
37
|
+
store.createSeries({
|
|
38
|
+
id: account.id,
|
|
39
|
+
accountId: account.id,
|
|
40
|
+
niche: account.niche,
|
|
41
|
+
language: account.language,
|
|
42
|
+
timezone: account.cadence.timezone,
|
|
43
|
+
minHoursBetweenPosts: account.cadence.minHoursBetweenPosts,
|
|
44
|
+
maxPostsPerDay: account.cadence.maxPostsPerDay,
|
|
45
|
+
windows: account.cadence.windows,
|
|
46
|
+
voices: account.voices,
|
|
47
|
+
hashtagPool: account.hashtagPool,
|
|
48
|
+
engageEnabled: account.engage.enabled,
|
|
49
|
+
allowSpicy: account.engage.allowSpicy,
|
|
50
|
+
maxRepliesPerDay: account.engage.maxRepliesPerDay,
|
|
51
|
+
minSecondsBetween: account.engage.minSecondsBetweenReplies,
|
|
52
|
+
});
|
|
53
|
+
store.db.prepare(`UPDATE desk_series SET base_json = ? WHERE id = ?`).run(JSON.stringify(account), account.id);
|
|
54
|
+
for (const [platform, config] of Object.entries(account.platforms)) {
|
|
55
|
+
if (!config) continue;
|
|
56
|
+
const kept: Record<string, unknown> = {};
|
|
57
|
+
for (const field of PLATFORM_FIELDS[platform] ?? []) {
|
|
58
|
+
const value = (config as unknown as Record<string, unknown>)[field];
|
|
59
|
+
if (value !== undefined) kept[field] = value;
|
|
60
|
+
}
|
|
61
|
+
store.setPlatform({ seriesId: account.id, platform, enabled: config.enabled, config: kept });
|
|
62
|
+
}
|
|
63
|
+
const sources: Array<[string, string]> = [
|
|
64
|
+
...account.sources.rss.map((value): [string, string] => ["rss", value]),
|
|
65
|
+
...account.sources.subreddits.map((value): [string, string] => ["subreddit", value]),
|
|
66
|
+
...account.sources.youtubeChannelIds.map((value): [string, string] => ["youtubeChannel", value]),
|
|
67
|
+
];
|
|
68
|
+
if (account.sources.youtubeTrendingRegion) sources.push(["youtubeTrending", account.sources.youtubeTrendingRegion]);
|
|
69
|
+
for (const [kind, value] of sources) store.addSource({ seriesId: account.id, kind, value });
|
|
70
|
+
store.db.exec("COMMIT");
|
|
71
|
+
added.push(account.id);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
store.db.exec("ROLLBACK");
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return added;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Imports the accounts file, then the first-run series when the desk still has none. */
|
|
81
|
+
export function seedDesk(store: Store, accounts: Account[]): void {
|
|
82
|
+
importAccounts(store, accounts);
|
|
83
|
+
seedFirstRunIfEmpty(store);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function seedFirstRunIfEmpty(store: Store): void {
|
|
87
|
+
if (!deskIsEmpty(store)) return;
|
|
88
|
+
importAccounts(store, [firstRunTemplate()]);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function deskIsEmpty(store: Store): boolean {
|
|
92
|
+
const row = store.db.prepare(`SELECT COUNT(*) AS n FROM desk_series`).get() as { n: number };
|
|
93
|
+
return Number(row.n) === 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The series of record, as pipeline accounts. */
|
|
97
|
+
export function deskAccounts(store: Store): Account[] {
|
|
98
|
+
const rows = store.db.prepare(`SELECT id, niche, language, base_json FROM desk_series ORDER BY id`).all() as Array<{
|
|
99
|
+
id: string;
|
|
100
|
+
niche: string;
|
|
101
|
+
language: string;
|
|
102
|
+
base_json: string | null;
|
|
103
|
+
}>;
|
|
104
|
+
const accounts: Account[] = [];
|
|
105
|
+
for (const row of rows) {
|
|
106
|
+
const base: Account = row.base_json
|
|
107
|
+
? (JSON.parse(row.base_json) as Account)
|
|
108
|
+
: { ...firstRunTemplate(), id: row.id, niche: row.niche, language: row.language };
|
|
109
|
+
const sources = store.db.prepare(`SELECT kind, value FROM desk_sources WHERE series_id = ? ORDER BY rowid`).all(row.id) as Array<{
|
|
110
|
+
kind: string;
|
|
111
|
+
value: string;
|
|
112
|
+
}>;
|
|
113
|
+
const pick = (kind: (typeof SOURCE_KINDS)[number]) => sources.filter((item) => item.kind === kind).map((item) => item.value);
|
|
114
|
+
const overlay = store.deskOverlay(row.id);
|
|
115
|
+
const withDesk = overlay ? applyDeskAccount(base, overlay) : base;
|
|
116
|
+
accounts.push({
|
|
117
|
+
...withDesk,
|
|
118
|
+
sources: {
|
|
119
|
+
rss: pick("rss"),
|
|
120
|
+
subreddits: pick("subreddit"),
|
|
121
|
+
youtubeChannelIds: pick("youtubeChannel"),
|
|
122
|
+
youtubeTrendingRegion: pick("youtubeTrending")[0] ?? null,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return accounts;
|
|
127
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { redact } from "../queue/seal.js";
|
|
4
|
+
import type { Store } from "../queue/store.js";
|
|
5
|
+
import { engineById, allEngineStatuses, DEFAULT_ENGINE, type EngineProbes } from "../product/engines.js";
|
|
6
|
+
import { readInsights } from "../product/insights.js";
|
|
7
|
+
import { channelInfo } from "./channel.js";
|
|
8
|
+
import { SECRET_FIELDS } from "./secrets.js";
|
|
9
|
+
import type { TickStatus } from "./tick-runner.js";
|
|
10
|
+
|
|
11
|
+
export interface StateInput {
|
|
12
|
+
store: Store;
|
|
13
|
+
dataDir: string;
|
|
14
|
+
env: Record<string, string | undefined>;
|
|
15
|
+
userId: string;
|
|
16
|
+
tick: TickStatus;
|
|
17
|
+
probes: EngineProbes;
|
|
18
|
+
/** The project this desk belongs to, or null for the machine desk (and for an env-chosen data folder). */
|
|
19
|
+
project?: { id: string; name: string } | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The path with the user's home folder written as ~. */
|
|
23
|
+
export function shortHome(dir: string, home: string): string {
|
|
24
|
+
if (home && (dir === home || dir.startsWith(home + path.sep))) return "~" + dir.slice(home.length);
|
|
25
|
+
return dir;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The full `State` of the desk API. Reads only. */
|
|
29
|
+
export async function buildState(input: StateInput): Promise<Record<string, unknown>> {
|
|
30
|
+
const snapshot = input.store.snapshot(input.userId);
|
|
31
|
+
let insights: Awaited<ReturnType<typeof readInsights>> = [];
|
|
32
|
+
try {
|
|
33
|
+
insights = await readInsights(path.join(input.dataDir, "product", "insights.json"));
|
|
34
|
+
} catch {
|
|
35
|
+
insights = [];
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
ok: true,
|
|
39
|
+
generatedAt: new Date().toISOString(),
|
|
40
|
+
home: input.dataDir,
|
|
41
|
+
// Who this server serves. `vfrey open`, `dashboard` and the tick summary match on it to reuse a dashboard.
|
|
42
|
+
dataDir: input.dataDir,
|
|
43
|
+
project: input.project ? { id: input.project.id, name: input.project.name } : null,
|
|
44
|
+
homeDisplay: shortHome(input.dataDir, input.env.HOME || os.homedir()),
|
|
45
|
+
appEnv: input.env.APP_ENV === "production" ? "production" : "qa",
|
|
46
|
+
...snapshot,
|
|
47
|
+
tick: input.tick,
|
|
48
|
+
// Public channel and page ids on file, shown so the person can type one to go live. Never a token.
|
|
49
|
+
channelIds: Object.fromEntries(
|
|
50
|
+
((snapshot.series as Array<{ id: string }>) ?? []).map((item) => [
|
|
51
|
+
item.id,
|
|
52
|
+
Object.fromEntries(channelInfo(input.store, item.id, input.env.CREDENTIALS_KEY).map((c) => [c.platform, c.id])),
|
|
53
|
+
]),
|
|
54
|
+
),
|
|
55
|
+
secretFields: { account: [...SECRET_FIELDS.account], series: [...SECRET_FIELDS.series] },
|
|
56
|
+
defaultEngine: defaultEngine(input.store),
|
|
57
|
+
engines: allEngineStatuses(input.probes).map((row) => ({ id: row.id, label: row.label, availability: row.availability, reason: row.reason, note: row.note })),
|
|
58
|
+
insights: insights.map((row) => ({
|
|
59
|
+
engineLabel: redact(row.engineLabel),
|
|
60
|
+
outputPath: row.outputPath,
|
|
61
|
+
bytes: row.bytes,
|
|
62
|
+
createdAt: row.createdAt,
|
|
63
|
+
...(row.subject !== undefined ? { subject: redact(row.subject) } : {}),
|
|
64
|
+
...(row.composition !== undefined ? { composition: row.composition } : {}),
|
|
65
|
+
...(row.durationSec !== undefined ? { durationSec: row.durationSec } : {}),
|
|
66
|
+
})),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The machine default, as a known engine id. */
|
|
71
|
+
function defaultEngine(store: Store): string {
|
|
72
|
+
try {
|
|
73
|
+
return engineById(store.engineDefault() ?? DEFAULT_ENGINE).id;
|
|
74
|
+
} catch {
|
|
75
|
+
return DEFAULT_ENGINE;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { SceneError } from "../scene/scene.js";
|
|
4
|
+
import { prepareEditCopy, startStudio, type StartStudioInput, type StudioHandle } from "../scene/studio.js";
|
|
5
|
+
import type { Store } from "../queue/store.js";
|
|
6
|
+
|
|
7
|
+
const ID = /^[A-Za-z0-9-]{1,64}$/;
|
|
8
|
+
const NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
9
|
+
|
|
10
|
+
export interface StudioRouteOptions {
|
|
11
|
+
store: Store;
|
|
12
|
+
dataDir: string;
|
|
13
|
+
env: Record<string, string | undefined>;
|
|
14
|
+
project: { id: string; name: string } | null;
|
|
15
|
+
/** Studios this dashboard started, by scene folder. They stop with the dashboard. */
|
|
16
|
+
running: Map<string, StudioHandle>;
|
|
17
|
+
/** Starts a studio. Tests inject a fake. */
|
|
18
|
+
start?: (input: StartStudioInput) => Promise<StudioHandle>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type StudioReply = { status: number; body: Record<string, unknown> };
|
|
22
|
+
|
|
23
|
+
const refusal = (status: number, message: string, fix: string, code?: string): StudioReply => ({ status, body: { ok: false, refusal: message, fix, ...(code ? { code } : {}) } });
|
|
24
|
+
|
|
25
|
+
function isDir(dir: string): boolean {
|
|
26
|
+
try {
|
|
27
|
+
return statSync(dir).isDirectory();
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The folder that holds a project's own scenes (`<project>/scenes`), or the data directory's `scenes` without a project. */
|
|
34
|
+
export function scenesRootFor(options: Pick<StudioRouteOptions, "dataDir" | "project">): string {
|
|
35
|
+
return options.project ? path.join(path.dirname(options.dataDir), "scenes") : path.join(options.dataDir, "scenes");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function open(options: StudioRouteOptions, input: StartStudioInput): Promise<StudioHandle> {
|
|
39
|
+
const known = options.running.get(input.dir);
|
|
40
|
+
if (known) return known;
|
|
41
|
+
const handle = await (options.start ?? startStudio)({ ...input, env: options.env });
|
|
42
|
+
options.running.set(input.dir, handle);
|
|
43
|
+
return handle;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const rendering = (dir: string) => `Render it as usual: npx vfrey render --scene ${dir} --engine vfrey-framey`;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* `POST /api/studio` with `{ run }` or `{ scene }`. A run's stored scene is never opened: it is copied to the project's
|
|
50
|
+
* `scenes/<name>-edit/` first (kept when it is already there, so earlier edits survive), with the run's timing and voice
|
|
51
|
+
* supplied. A scene named by `scene` is a folder of the project's own and is edited where it is.
|
|
52
|
+
*/
|
|
53
|
+
export async function studioRoute(options: StudioRouteOptions, body: Record<string, unknown>): Promise<StudioReply> {
|
|
54
|
+
const runId = typeof body.run === "string" ? body.run : null;
|
|
55
|
+
const sceneName = typeof body.scene === "string" ? body.scene : null;
|
|
56
|
+
try {
|
|
57
|
+
if (runId) {
|
|
58
|
+
if (!ID.test(runId)) return refusal(404, "No run has that id.", "Open a run from the Runs list.");
|
|
59
|
+
const row = options.store.db.prepare(`SELECT payload FROM jobs WHERE id = ?`).get(runId) as { payload: string } | undefined;
|
|
60
|
+
if (!row) return refusal(404, "No run has that id.", "Open a run from the Runs list.");
|
|
61
|
+
let payload: {
|
|
62
|
+
scene?: { source?: string; dir?: string | null; kind?: string | null } | null;
|
|
63
|
+
content?: { title?: string; voiceoverScript?: string; voiceoverPath?: string | null; beats?: Array<{ role?: string; text?: string; caption?: string }> } | null;
|
|
64
|
+
};
|
|
65
|
+
try {
|
|
66
|
+
payload = JSON.parse(row.payload);
|
|
67
|
+
} catch {
|
|
68
|
+
return refusal(404, "That run has no scene.", "Open a run that has an agent-written scene.");
|
|
69
|
+
}
|
|
70
|
+
const scene = payload.scene;
|
|
71
|
+
if (!scene || scene.source !== "agent" || !scene.dir) return refusal(404, "That run used the default template, so there is no scene to edit.", "Write a scene (`npx vfrey scene init DIR`) and run with --scene DIR.");
|
|
72
|
+
if (scene.kind !== "html") {
|
|
73
|
+
return refusal(409, "The studio edits HTML scenes. This is a Remotion scene (index.tsx).", "edit index.tsx in your own editor, or write an HTML scene to use the studio.", "scene_studio_remotion");
|
|
74
|
+
}
|
|
75
|
+
if (!isDir(scene.dir)) return refusal(404, "The run's stored scene copy is no longer on this machine.", "Run it again, or open a scene from the project's scenes folder.");
|
|
76
|
+
const content = payload.content ?? {};
|
|
77
|
+
const root = options.project ? path.dirname(options.dataDir) : options.dataDir;
|
|
78
|
+
const voice = typeof content.voiceoverPath === "string" && content.voiceoverPath ? path.resolve(root, content.voiceoverPath) : null;
|
|
79
|
+
const copy = await prepareEditCopy({ storedDir: scene.dir, scenesRoot: scenesRootFor(options), name: path.basename(scene.dir) });
|
|
80
|
+
const handle = await open(options, {
|
|
81
|
+
dir: copy.dir,
|
|
82
|
+
supply: {
|
|
83
|
+
title: content.title ?? "",
|
|
84
|
+
lines: String(content.voiceoverScript ?? "").split("\n").map((line) => line.trim()).filter(Boolean),
|
|
85
|
+
beats: (content.beats ?? []).map((b) => ({ role: String(b.role ?? ""), text: String(b.text ?? ""), caption: String(b.caption ?? "") })),
|
|
86
|
+
voiceFile: voice && existsSync(voice) ? voice : null,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
const message = copy.copied
|
|
90
|
+
? `Opened a copy of this run's scene at ${copy.dir}. Your edits are saved there; the run's stored copy is not changed. ${rendering(copy.dir)}`
|
|
91
|
+
: `Opened your earlier edit copy at ${copy.dir} (the run's stored copy is not changed). ${rendering(copy.dir)}`;
|
|
92
|
+
return { status: 200, body: { ok: true, url: handle.url, dir: copy.dir, copied: copy.copied, message, warnings: copy.warnings } };
|
|
93
|
+
}
|
|
94
|
+
if (sceneName) {
|
|
95
|
+
if (!options.project) return refusal(404, "This dashboard has no project, so it has no scenes folder.", "Open the dashboard of a project (`npx vfrey open` inside it).");
|
|
96
|
+
if (!NAME.test(sceneName)) return refusal(404, "No scene has that name.", "Use a folder name from the project's scenes folder.");
|
|
97
|
+
const dir = path.join(scenesRootFor(options), sceneName);
|
|
98
|
+
if (!isDir(dir) || !existsSync(path.join(dir, "index.html"))) {
|
|
99
|
+
const remotion = existsSync(path.join(dir, "index.tsx"));
|
|
100
|
+
return remotion
|
|
101
|
+
? refusal(409, "The studio edits HTML scenes. This is a Remotion scene (index.tsx).", "edit index.tsx in your own editor, or write an HTML scene to use the studio.", "scene_studio_remotion")
|
|
102
|
+
: refusal(404, "No HTML scene has that name in this project's scenes folder.", "Check the name against the project's scenes folder.");
|
|
103
|
+
}
|
|
104
|
+
const handle = await open(options, { dir });
|
|
105
|
+
return { status: 200, body: { ok: true, url: handle.url, dir, copied: false, message: `Opened ${dir}. Edits are saved there. ${rendering(dir)}`, warnings: [] } };
|
|
106
|
+
}
|
|
107
|
+
return refusal(400, "Say which scene to edit.", "Send { run: <run id> } or { scene: <name> }.");
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (error instanceof SceneError) return refusal(409, error.message, error.fix, error.code);
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Stops every studio the dashboard started. */
|
|
115
|
+
export async function closeStudios(running: Map<string, StudioHandle>): Promise<void> {
|
|
116
|
+
const all = [...running.values()];
|
|
117
|
+
running.clear();
|
|
118
|
+
await Promise.allSettled(all.map((studio) => studio.close()));
|
|
119
|
+
}
|