@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
package/src/config.ts
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { ConfigError } from "@vfrey/contracts";
|
|
3
|
+
import type { Account, AppConfig } from "@vfrey/contracts";
|
|
4
|
+
import { dataDirFor } from "./home.js";
|
|
5
|
+
|
|
6
|
+
type Env = Record<string, string | undefined>;
|
|
7
|
+
|
|
8
|
+
function requiredString(value: unknown, path: string): string {
|
|
9
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
10
|
+
throw new ConfigError("account_config", `${path} must be a non-empty string`);
|
|
11
|
+
}
|
|
12
|
+
return value.trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function optionalString(value: unknown): string | null {
|
|
16
|
+
if (typeof value !== "string" || value.trim() === "") return null;
|
|
17
|
+
return value.trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function stringList(value: unknown, path: string): string[] {
|
|
21
|
+
if (value === undefined) return [];
|
|
22
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
23
|
+
throw new ConfigError("account_config", `${path} must be a string array`);
|
|
24
|
+
}
|
|
25
|
+
return value.map((item) => item.trim()).filter(Boolean);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function rejectInlineSecrets(value: unknown, path: string): void {
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
value.forEach((item, index) => rejectInlineSecrets(item, `${path}[${index}]`));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (!value || typeof value !== "object") return;
|
|
34
|
+
for (const [key, child] of Object.entries(value)) {
|
|
35
|
+
const childPath = `${path}.${key}`;
|
|
36
|
+
if (typeof child === "string") {
|
|
37
|
+
const secretKey = /token|secret|key/i.test(key);
|
|
38
|
+
const envName = key.endsWith("Env");
|
|
39
|
+
if (secretKey && !envName) {
|
|
40
|
+
throw new ConfigError(
|
|
41
|
+
"inline_secret",
|
|
42
|
+
`${childPath} looks like a credential field. Point it at an environment variable name ending in Env.`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
if (/^(ya29\.|act\.|sk-|xox[bap]-)/.test(child)) {
|
|
46
|
+
throw new ConfigError("inline_secret", `${childPath} contains a credential value. Store it in the environment.`);
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
rejectInlineSecrets(child, childPath);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function parseAccounts(raw: unknown): Account[] {
|
|
55
|
+
rejectInlineSecrets(raw, "accounts");
|
|
56
|
+
const root = raw as { accounts?: unknown };
|
|
57
|
+
if (!root || !Array.isArray(root.accounts) || root.accounts.length === 0) {
|
|
58
|
+
throw new ConfigError("account_config", "accounts file must contain a non-empty accounts array");
|
|
59
|
+
}
|
|
60
|
+
return root.accounts.map((entry, index) => parseAccount(entry, index));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseAccount(entry: unknown, index: number): Account {
|
|
64
|
+
const path = `accounts[${index}]`;
|
|
65
|
+
const row = entry as Record<string, unknown>;
|
|
66
|
+
const cadence = (row.cadence ?? {}) as Record<string, unknown>;
|
|
67
|
+
const sources = (row.sources ?? {}) as Record<string, unknown>;
|
|
68
|
+
const platforms = (row.platforms ?? {}) as Record<string, unknown>;
|
|
69
|
+
const engage = (row.engage ?? {}) as Record<string, unknown>;
|
|
70
|
+
const voices = stringList(row.voices, `${path}.voices`);
|
|
71
|
+
if (voices.length === 0) {
|
|
72
|
+
throw new ConfigError("account_config", `${path}.voices needs at least one voice id`);
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
id: requiredString(row.id, `${path}.id`),
|
|
76
|
+
niche: requiredString(row.niche, `${path}.niche`),
|
|
77
|
+
language: optionalString(row.language) ?? "en",
|
|
78
|
+
cadence: {
|
|
79
|
+
timezone: requiredString(cadence.timezone, `${path}.cadence.timezone`),
|
|
80
|
+
minHoursBetweenPosts: numberOr(cadence.minHoursBetweenPosts, 18),
|
|
81
|
+
maxPostsPerDay: numberOr(cadence.maxPostsPerDay, 1),
|
|
82
|
+
windows: stringList(cadence.windows, `${path}.cadence.windows`),
|
|
83
|
+
},
|
|
84
|
+
voices,
|
|
85
|
+
hashtagPool: stringList(row.hashtagPool, `${path}.hashtagPool`),
|
|
86
|
+
sources: {
|
|
87
|
+
rss: stringList(sources.rss, `${path}.sources.rss`),
|
|
88
|
+
subreddits: stringList(sources.subreddits, `${path}.sources.subreddits`),
|
|
89
|
+
youtubeChannelIds: stringList(sources.youtubeChannelIds, `${path}.sources.youtubeChannelIds`),
|
|
90
|
+
youtubeTrendingRegion: optionalString(sources.youtubeTrendingRegion),
|
|
91
|
+
},
|
|
92
|
+
platforms: {
|
|
93
|
+
youtube: platforms.youtube ? parseYouTube(platforms.youtube, `${path}.platforms.youtube`) : undefined,
|
|
94
|
+
tiktok: platforms.tiktok ? parseTikTok(platforms.tiktok, `${path}.platforms.tiktok`) : undefined,
|
|
95
|
+
instagram: platforms.instagram ? parseInstagram(platforms.instagram, `${path}.platforms.instagram`) : undefined,
|
|
96
|
+
facebook: platforms.facebook ? parseFacebook(platforms.facebook, `${path}.platforms.facebook`) : undefined,
|
|
97
|
+
},
|
|
98
|
+
engage: {
|
|
99
|
+
enabled: engage.enabled !== false,
|
|
100
|
+
allowSpicy: engage.allowSpicy === true,
|
|
101
|
+
maxRepliesPerDay: numberOr(engage.maxRepliesPerDay, 20),
|
|
102
|
+
minSecondsBetweenReplies: numberOr(engage.minSecondsBetweenReplies, 45),
|
|
103
|
+
},
|
|
104
|
+
waiveAudioRotation: row.waiveAudioRotation === true,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parseYouTube(value: unknown, path: string): Account["platforms"]["youtube"] {
|
|
109
|
+
const row = value as Record<string, unknown>;
|
|
110
|
+
const privacy = optionalString(row.privacyStatus) ?? "private";
|
|
111
|
+
if (privacy !== "public" && privacy !== "private" && privacy !== "unlisted") {
|
|
112
|
+
throw new ConfigError("account_config", `${path}.privacyStatus is invalid`);
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
enabled: row.enabled !== false,
|
|
116
|
+
privacyStatus: privacy,
|
|
117
|
+
categoryId: optionalString(row.categoryId) ?? "22",
|
|
118
|
+
madeForKids: row.madeForKids === true,
|
|
119
|
+
refreshTokenEnv: requiredString(row.refreshTokenEnv, `${path}.refreshTokenEnv`),
|
|
120
|
+
clientIdEnv: requiredString(row.clientIdEnv, `${path}.clientIdEnv`),
|
|
121
|
+
clientSecretEnv: requiredString(row.clientSecretEnv, `${path}.clientSecretEnv`),
|
|
122
|
+
notifySubscribers: typeof row.notifySubscribers === "boolean" ? row.notifySubscribers : null,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function parseTikTok(value: unknown, path: string): Account["platforms"]["tiktok"] {
|
|
127
|
+
const row = value as Record<string, unknown>;
|
|
128
|
+
const privacy = optionalString(row.privacyLevel) ?? "SELF_ONLY";
|
|
129
|
+
const allowed = ["PUBLIC_TO_EVERYONE", "MUTUAL_FOLLOW_FRIENDS", "FOLLOWER_OF_CREATOR", "SELF_ONLY"];
|
|
130
|
+
if (!allowed.includes(privacy)) throw new ConfigError("account_config", `${path}.privacyLevel is invalid`);
|
|
131
|
+
return {
|
|
132
|
+
enabled: row.enabled !== false,
|
|
133
|
+
privacyLevel: privacy as "SELF_ONLY",
|
|
134
|
+
refreshTokenEnv: requiredString(row.refreshTokenEnv, `${path}.refreshTokenEnv`),
|
|
135
|
+
openIdEnv: requiredString(row.openIdEnv, `${path}.openIdEnv`),
|
|
136
|
+
brandContent: row.brandContent === true,
|
|
137
|
+
brandOrganic: row.brandOrganic === true,
|
|
138
|
+
disableComment: row.disableComment === true,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function parseInstagram(value: unknown, path: string): Account["platforms"]["instagram"] {
|
|
143
|
+
const row = value as Record<string, unknown>;
|
|
144
|
+
return {
|
|
145
|
+
enabled: row.enabled !== false,
|
|
146
|
+
igUserIdEnv: requiredString(row.igUserIdEnv, `${path}.igUserIdEnv`),
|
|
147
|
+
accessTokenEnv: requiredString(row.accessTokenEnv, `${path}.accessTokenEnv`),
|
|
148
|
+
shareToFeed: row.shareToFeed !== false,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function parseFacebook(value: unknown, path: string): Account["platforms"]["facebook"] {
|
|
153
|
+
const row = value as Record<string, unknown>;
|
|
154
|
+
return {
|
|
155
|
+
enabled: row.enabled !== false,
|
|
156
|
+
pageIdEnv: requiredString(row.pageIdEnv, `${path}.pageIdEnv`),
|
|
157
|
+
pageTokenEnv: requiredString(row.pageTokenEnv, `${path}.pageTokenEnv`),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function numberOr(value: unknown, fallback: number): number {
|
|
162
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
163
|
+
return value;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function loadConfig(env: Env, accountsJson?: string): AppConfig {
|
|
167
|
+
const accountsFile = env.ACCOUNTS_FILE || "accounts.json";
|
|
168
|
+
const raw = accountsJson ?? (existsSync(accountsFile) ? readFileSync(accountsFile, "utf8") : null);
|
|
169
|
+
const accounts = raw === null ? [] : parseAccounts(JSON.parse(raw) as unknown);
|
|
170
|
+
const provider = env.LLM_PROVIDER || "mock";
|
|
171
|
+
return {
|
|
172
|
+
appEnv: env.APP_ENV === "production" ? "production" : "qa",
|
|
173
|
+
dataDir: dataDirFor(env),
|
|
174
|
+
accountsFile,
|
|
175
|
+
dryRun: env.PIPELINE_DRY_RUN === "true",
|
|
176
|
+
llm: {
|
|
177
|
+
provider,
|
|
178
|
+
baseUrl: env.LLM_BASE_URL || defaultLlmBase(provider),
|
|
179
|
+
apiKey: env.LLM_API_KEY || null,
|
|
180
|
+
model: env.LLM_MODEL || defaultLlmModel(provider),
|
|
181
|
+
},
|
|
182
|
+
tts: {
|
|
183
|
+
provider: env.TTS_PROVIDER === "openai" ? "openai" : env.TTS_PROVIDER === "none" ? "none" : "local",
|
|
184
|
+
baseUrl: env.TTS_BASE_URL || "https://api.openai.com/v1",
|
|
185
|
+
apiKey: env.TTS_API_KEY || env.LLM_API_KEY || null,
|
|
186
|
+
model: env.TTS_MODEL || "tts-1",
|
|
187
|
+
} as AppConfig["tts"],
|
|
188
|
+
googleApiKey: env.YOUTUBE_API_KEY || null,
|
|
189
|
+
reddit: {
|
|
190
|
+
clientId: env.REDDIT_CLIENT_ID || null,
|
|
191
|
+
clientSecret: env.REDDIT_CLIENT_SECRET || null,
|
|
192
|
+
userAgent: env.REDDIT_USER_AGENT || "vfrey:v1 (by /u/operator)",
|
|
193
|
+
},
|
|
194
|
+
tiktok: {
|
|
195
|
+
clientKey: env.TIKTOK_CLIENT_KEY || null,
|
|
196
|
+
clientSecret: env.TIKTOK_CLIENT_SECRET || null,
|
|
197
|
+
clientAudited: env.TIKTOK_CLIENT_AUDITED === "true",
|
|
198
|
+
},
|
|
199
|
+
meta: {
|
|
200
|
+
graphVersion: env.META_GRAPH_VERSION || "v26.0",
|
|
201
|
+
appId: env.META_APP_ID || null,
|
|
202
|
+
appSecret: env.META_APP_SECRET || null,
|
|
203
|
+
},
|
|
204
|
+
youtube: {
|
|
205
|
+
dailyUploadCap: Number(env.YOUTUBE_DAILY_UPLOAD_CAP || "90"),
|
|
206
|
+
},
|
|
207
|
+
accounts,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function defaultLlmBase(provider: string): string {
|
|
212
|
+
switch (provider) {
|
|
213
|
+
case "openai":
|
|
214
|
+
return "https://api.openai.com/v1";
|
|
215
|
+
case "groq":
|
|
216
|
+
return "https://api.groq.com/openai/v1";
|
|
217
|
+
case "xai":
|
|
218
|
+
return "https://api.x.ai/v1";
|
|
219
|
+
case "gemini":
|
|
220
|
+
return "https://generativelanguage.googleapis.com/v1beta/openai";
|
|
221
|
+
case "anthropic":
|
|
222
|
+
return "https://api.anthropic.com";
|
|
223
|
+
default:
|
|
224
|
+
return "";
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function defaultLlmModel(provider: string): string {
|
|
229
|
+
switch (provider) {
|
|
230
|
+
case "anthropic":
|
|
231
|
+
return "claude-sonnet-5-5";
|
|
232
|
+
case "gemini":
|
|
233
|
+
return "gemini-2.5-flash";
|
|
234
|
+
case "groq":
|
|
235
|
+
return "llama-3.3-70b-versatile";
|
|
236
|
+
case "xai":
|
|
237
|
+
return "grok-4";
|
|
238
|
+
case "openai":
|
|
239
|
+
return "gpt-4.1-mini";
|
|
240
|
+
default:
|
|
241
|
+
return "mock";
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function envValue(env: Env, name: string): string {
|
|
246
|
+
const value = env[name];
|
|
247
|
+
if (!value) throw new ConfigError("missing_env", `Environment variable ${name} is not set`);
|
|
248
|
+
return value;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export interface Refusal {
|
|
252
|
+
message: string;
|
|
253
|
+
fix: string;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function platformEnvNames(account: Account): string[] {
|
|
257
|
+
const names: string[] = [];
|
|
258
|
+
const p = account.platforms;
|
|
259
|
+
if (p.youtube?.enabled) names.push(p.youtube.refreshTokenEnv, p.youtube.clientIdEnv, p.youtube.clientSecretEnv);
|
|
260
|
+
if (p.tiktok?.enabled) names.push(p.tiktok.refreshTokenEnv, p.tiktok.openIdEnv);
|
|
261
|
+
if (p.instagram?.enabled) names.push(p.instagram.igUserIdEnv, p.instagram.accessTokenEnv);
|
|
262
|
+
if (p.facebook?.enabled) names.push(p.facebook.pageIdEnv, p.facebook.pageTokenEnv);
|
|
263
|
+
return names;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* The mock writer produces canned copy. Outside a dry run it must never feed a platform
|
|
268
|
+
* the operator has keys for. Returns the refusal, or null when the run may go on.
|
|
269
|
+
*/
|
|
270
|
+
export function mockWriterRefusal(config: AppConfig, env: Env, dryRun: boolean): Refusal | null {
|
|
271
|
+
if (dryRun) return null;
|
|
272
|
+
if (config.llm.provider !== "mock" && config.llm.provider !== "none") return null;
|
|
273
|
+
const keyed = config.accounts.some((account) => platformEnvNames(account).some((name) => Boolean(env[name])));
|
|
274
|
+
if (!keyed) return null;
|
|
275
|
+
return {
|
|
276
|
+
message: "LLM_PROVIDER is mock and a platform key is set. The mock writer would publish canned copy.",
|
|
277
|
+
fix: "Set LLM_PROVIDER and LLM_API_KEY to a real model, or run with --dry-run.",
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const WINDOW = /^(\d{2}):(\d{2})-(\d{2}):(\d{2})$/;
|
|
282
|
+
|
|
283
|
+
/** Returns a sentence naming the first fault in a cadence, or null when it is usable. */
|
|
284
|
+
export function validateCadence(cadence: {
|
|
285
|
+
timezone: string;
|
|
286
|
+
minHoursBetweenPosts: number;
|
|
287
|
+
maxPostsPerDay: number;
|
|
288
|
+
windows: string[];
|
|
289
|
+
}): string | null {
|
|
290
|
+
try {
|
|
291
|
+
new Intl.DateTimeFormat("en-US", { timeZone: cadence.timezone });
|
|
292
|
+
} catch {
|
|
293
|
+
return `timezone ${JSON.stringify(cadence.timezone)} is not a valid IANA timezone`;
|
|
294
|
+
}
|
|
295
|
+
if (!Number.isFinite(cadence.minHoursBetweenPosts) || cadence.minHoursBetweenPosts < 0) {
|
|
296
|
+
return "minHoursBetweenPosts must be a number of hours, 0 or more";
|
|
297
|
+
}
|
|
298
|
+
if (!Number.isFinite(cadence.maxPostsPerDay) || cadence.maxPostsPerDay < 0) {
|
|
299
|
+
return "maxPostsPerDay must be a number, 0 or more";
|
|
300
|
+
}
|
|
301
|
+
for (const window of cadence.windows) {
|
|
302
|
+
const match = WINDOW.exec(window);
|
|
303
|
+
const ok =
|
|
304
|
+
match !== null &&
|
|
305
|
+
Number(match[1]) < 24 && Number(match[3]) < 24 && Number(match[2]) < 60 && Number(match[4]) < 60;
|
|
306
|
+
if (!ok) return `window ${JSON.stringify(window)} must look like 09:00-17:30`;
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import net from "node:net";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { canonicalPath } from "./project/canonical.js";
|
|
5
|
+
import type { ResolvedHome } from "./home.js";
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_DASHBOARD_PORT = 4317;
|
|
8
|
+
|
|
9
|
+
type Env = Record<string, string | undefined>;
|
|
10
|
+
|
|
11
|
+
/** The port `npx vfrey dashboard` tries first without --port. VFREY_DASHBOARD_PORT moves it, for a machine where 4317 is busy. */
|
|
12
|
+
export function defaultDashboardPort(env: Env): number {
|
|
13
|
+
const port = Number(env.VFREY_DASHBOARD_PORT);
|
|
14
|
+
return Number.isInteger(port) && port > 0 && port < 65536 ? port : DEFAULT_DASHBOARD_PORT;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Which desk a dashboard serves: its data folder, and the project when a project decided that folder. */
|
|
18
|
+
export interface Desk {
|
|
19
|
+
dataDir: string;
|
|
20
|
+
project: { id: string; name: string } | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function deskOf(home: Pick<ResolvedHome, "dataDir" | "project">): Desk {
|
|
24
|
+
return { dataDir: home.dataDir, project: home.project ? { id: home.project.id, name: home.project.name } : null };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** What `.vfrey/dashboard.json` (or the machine desk's `dashboard.json`) holds. */
|
|
28
|
+
export interface DashboardRecord {
|
|
29
|
+
port: number;
|
|
30
|
+
url: string;
|
|
31
|
+
pid: number;
|
|
32
|
+
projectId: string | null;
|
|
33
|
+
startedAt: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const recordFile = (dataDir: string) => path.join(dataDir, "dashboard.json");
|
|
37
|
+
|
|
38
|
+
export function readDashboardRecord(dataDir: string): DashboardRecord | null {
|
|
39
|
+
try {
|
|
40
|
+
const raw = JSON.parse(readFileSync(recordFile(dataDir), "utf8")) as Partial<DashboardRecord>;
|
|
41
|
+
if (typeof raw.port !== "number" || !Number.isInteger(raw.port) || raw.port <= 0 || raw.port >= 65536) return null;
|
|
42
|
+
return {
|
|
43
|
+
port: raw.port,
|
|
44
|
+
url: typeof raw.url === "string" ? raw.url : `http://127.0.0.1:${raw.port}`,
|
|
45
|
+
pid: typeof raw.pid === "number" ? raw.pid : 0,
|
|
46
|
+
projectId: typeof raw.projectId === "string" ? raw.projectId : null,
|
|
47
|
+
startedAt: typeof raw.startedAt === "string" ? raw.startedAt : "",
|
|
48
|
+
};
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The dashboard notes itself in the data folder. The write is atomic, and the last dashboard to start owns the record. */
|
|
55
|
+
export function writeDashboardRecord(dataDir: string, port: number, projectId: string | null = null, pid: number = process.pid): void {
|
|
56
|
+
try {
|
|
57
|
+
mkdirSync(dataDir, { recursive: true });
|
|
58
|
+
const record: DashboardRecord = { port, url: `http://127.0.0.1:${port}`, pid, projectId, startedAt: new Date().toISOString() };
|
|
59
|
+
const tmp = `${recordFile(dataDir)}.${pid}.tmp`;
|
|
60
|
+
writeFileSync(tmp, JSON.stringify(record) + "\n");
|
|
61
|
+
renameSync(tmp, recordFile(dataDir));
|
|
62
|
+
} catch {
|
|
63
|
+
/* the record is a convenience, never a reason to stop the dashboard */
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Removes the record only while it still names this dashboard (pid and port). A newer dashboard's record is left alone. */
|
|
68
|
+
export function removeDashboardRecord(dataDir: string, port?: number, pid: number = process.pid): void {
|
|
69
|
+
try {
|
|
70
|
+
const record = readDashboardRecord(dataDir);
|
|
71
|
+
if (!record || record.pid !== pid) return;
|
|
72
|
+
if (port !== undefined && record.port !== port) return;
|
|
73
|
+
rmSync(recordFile(dataDir), { force: true });
|
|
74
|
+
} catch {
|
|
75
|
+
/* no record, or not ours */
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const canon = canonicalPath;
|
|
80
|
+
|
|
81
|
+
/** Whether a GET /api/state body names this desk: the same data folder and the same project id (or both none). */
|
|
82
|
+
export function stateMatchesDesk(state: unknown, desk: Desk): boolean {
|
|
83
|
+
if (!state || typeof state !== "object") return false;
|
|
84
|
+
const body = state as { ok?: unknown; dataDir?: unknown; project?: { id?: unknown } | null };
|
|
85
|
+
if (body.ok !== true || typeof body.dataDir !== "string") return false;
|
|
86
|
+
if (canon(body.dataDir) !== canon(desk.dataDir)) return false;
|
|
87
|
+
const served = body.project && typeof body.project.id === "string" ? body.project.id : null;
|
|
88
|
+
return served === (desk.project?.id ?? null);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Probes one origin. True only when it answers /api/state and says it serves this desk. */
|
|
92
|
+
export async function servesDesk(origin: string, desk: Desk, timeoutMs = 300): Promise<boolean> {
|
|
93
|
+
try {
|
|
94
|
+
const response = await fetch(`${origin}/api/state`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
95
|
+
if (!response.ok) return false;
|
|
96
|
+
return stateMatchesDesk(await response.json(), desk);
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The origin of a dashboard that is listening and serves this desk, or null. It tries the port the desk recorded,
|
|
104
|
+
* then the default one. A dashboard that serves another desk (another project, or the machine desk) does not count.
|
|
105
|
+
*/
|
|
106
|
+
export async function findDashboard(desk: Desk, env: Env = process.env, timeoutMs = 300): Promise<string | null> {
|
|
107
|
+
const ports = [...new Set([readDashboardRecord(desk.dataDir)?.port ?? null, defaultDashboardPort(env)].filter((port): port is number => port !== null))];
|
|
108
|
+
for (const port of ports) {
|
|
109
|
+
const origin = `http://127.0.0.1:${port}`;
|
|
110
|
+
if (await servesDesk(origin, desk, timeoutMs)) return origin;
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Whether a TCP port on loopback can be bound right now. */
|
|
116
|
+
export function canListen(port: number): Promise<boolean> {
|
|
117
|
+
return new Promise((resolve) => {
|
|
118
|
+
const server = net.createServer();
|
|
119
|
+
server.once("error", () => resolve(false));
|
|
120
|
+
server.listen(port, "127.0.0.1", () => server.close(() => resolve(true)));
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The ports to try, in order: the one the desk recorded, then the default, then the ones above the default. */
|
|
125
|
+
export function portCandidates(recorded: number | null, preferred: number, span = 50): number[] {
|
|
126
|
+
const list = [...(recorded !== null ? [recorded] : []), preferred];
|
|
127
|
+
for (let offset = 1; offset <= span && preferred + offset < 65536; offset += 1) list.push(preferred + offset);
|
|
128
|
+
return [...new Set(list)];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The first candidate that is free: the recorded port, else 4317 (VFREY_DASHBOARD_PORT), else the next free port up. */
|
|
132
|
+
export async function pickPort(desk: Desk, env: Env, isFree: (port: number) => Promise<boolean> = canListen, skip: ReadonlySet<number> = new Set()): Promise<number> {
|
|
133
|
+
const candidates = portCandidates(readDashboardRecord(desk.dataDir)?.port ?? null, defaultDashboardPort(env));
|
|
134
|
+
for (const port of candidates) {
|
|
135
|
+
if (skip.has(port)) continue;
|
|
136
|
+
if (await isFree(port)) return port;
|
|
137
|
+
}
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const lockFile = (dataDir: string) => path.join(dataDir, "dashboard.lock");
|
|
142
|
+
|
|
143
|
+
function alive(pid: number): boolean {
|
|
144
|
+
try {
|
|
145
|
+
process.kill(pid, 0);
|
|
146
|
+
return true;
|
|
147
|
+
} catch (error) {
|
|
148
|
+
return (error as NodeJS.ErrnoException).code === "EPERM";
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* A short lock held while a dashboard picks its port and writes its record, so two starts at once (a tick and `open`)
|
|
154
|
+
* do not become two dashboards for one desk. Returns a release function, or null when another live start holds it.
|
|
155
|
+
*/
|
|
156
|
+
export function acquireStartLock(dataDir: string, staleMs = 20_000): (() => void) | null {
|
|
157
|
+
mkdirSync(dataDir, { recursive: true });
|
|
158
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
159
|
+
try {
|
|
160
|
+
writeFileSync(lockFile(dataDir), String(process.pid), { flag: "wx" });
|
|
161
|
+
return () => {
|
|
162
|
+
try {
|
|
163
|
+
if (readFileSync(lockFile(dataDir), "utf8") === String(process.pid)) rmSync(lockFile(dataDir), { force: true });
|
|
164
|
+
} catch {
|
|
165
|
+
/* already gone */
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
} catch {
|
|
169
|
+
try {
|
|
170
|
+
const holder = Number(readFileSync(lockFile(dataDir), "utf8"));
|
|
171
|
+
const age = Date.now() - statSync(lockFile(dataDir)).mtimeMs;
|
|
172
|
+
if (holder && alive(holder) && age < staleMs) return null;
|
|
173
|
+
rmSync(lockFile(dataDir), { force: true });
|
|
174
|
+
} catch {
|
|
175
|
+
/* the holder released it between the two reads: try again */
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { closeSync, mkdirSync, openSync, readFileSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { findDashboard, type Desk } from "./dashboard-link.js";
|
|
7
|
+
|
|
8
|
+
type Env = Record<string, string | undefined>;
|
|
9
|
+
|
|
10
|
+
export type EnsureResult = { ok: true; origin: string; started: boolean } | { ok: false; message: string; fix: string };
|
|
11
|
+
|
|
12
|
+
export interface EnsureOptions {
|
|
13
|
+
desk: Desk;
|
|
14
|
+
env: Env;
|
|
15
|
+
/** The folder the dashboard process starts in, so it finds the same project. Default is this process's folder. */
|
|
16
|
+
cwd?: string;
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
/** Test hook: how to start the detached dashboard. Default spawns this CLI. */
|
|
19
|
+
spawnDashboard?: (logFile: string) => { pid?: number; exited: Promise<number | null> };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const dashboardLogFile = (dataDir: string): string => path.join(dataDir, "dashboard.log");
|
|
23
|
+
|
|
24
|
+
/** Starts `vfrey dashboard` as its own process: output to the log file, no stdin, and it outlives this process. */
|
|
25
|
+
function spawnDetached(logFile: string, env: Env, cwd: string): { pid?: number; exited: Promise<number | null> } {
|
|
26
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const tsx = createRequire(import.meta.url).resolve("tsx");
|
|
28
|
+
mkdirSync(path.dirname(logFile), { recursive: true });
|
|
29
|
+
const fd = openSync(logFile, "a");
|
|
30
|
+
const child = spawn(
|
|
31
|
+
process.execPath,
|
|
32
|
+
["--experimental-sqlite", "--disable-warning=ExperimentalWarning", "--import", tsx, path.join(here, "cli.ts"), "dashboard"],
|
|
33
|
+
{ cwd, env: env as NodeJS.ProcessEnv, stdio: ["ignore", fd, fd], detached: true },
|
|
34
|
+
);
|
|
35
|
+
closeSync(fd);
|
|
36
|
+
const exited = new Promise<number | null>((resolve) => {
|
|
37
|
+
child.once("exit", (code) => resolve(code));
|
|
38
|
+
child.once("error", () => resolve(1));
|
|
39
|
+
});
|
|
40
|
+
child.unref();
|
|
41
|
+
return { pid: child.pid, exited };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function logTail(file: string): string {
|
|
45
|
+
try {
|
|
46
|
+
return readFileSync(file, "utf8").trim().split("\n").slice(-3).join(" | ");
|
|
47
|
+
} catch {
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The one path to a live dashboard for a desk, used by `open` and by the tick summary: reuse the one that is listening,
|
|
54
|
+
* else start one detached and wait until /api/state answers and names this desk.
|
|
55
|
+
*/
|
|
56
|
+
export async function ensureDashboard(options: EnsureOptions): Promise<EnsureResult> {
|
|
57
|
+
const { desk, env } = options;
|
|
58
|
+
const existing = await findDashboard(desk, env);
|
|
59
|
+
if (existing) return { ok: true, origin: existing, started: false };
|
|
60
|
+
const logFile = dashboardLogFile(desk.dataDir);
|
|
61
|
+
let child;
|
|
62
|
+
try {
|
|
63
|
+
child = (options.spawnDashboard ?? ((file) => spawnDetached(file, env, options.cwd ?? process.cwd())))(logFile);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
return { ok: false, message: `Could not start the dashboard: ${error instanceof Error ? error.message : String(error)}.`, fix: "run `npx vfrey doctor`, or start it by hand with `npx vfrey dashboard`." };
|
|
66
|
+
}
|
|
67
|
+
let exitCode: number | null | undefined;
|
|
68
|
+
void child.exited.then((code) => {
|
|
69
|
+
exitCode = code;
|
|
70
|
+
});
|
|
71
|
+
const deadline = Date.now() + (options.timeoutMs ?? 15_000);
|
|
72
|
+
while (Date.now() < deadline) {
|
|
73
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
74
|
+
const origin = await findDashboard(desk, env);
|
|
75
|
+
if (origin) return { ok: true, origin, started: true };
|
|
76
|
+
// A child that exited without a dashboard having come up for this desk has failed; its log says why.
|
|
77
|
+
if (exitCode !== undefined) {
|
|
78
|
+
const origin = await findDashboard(desk, env, 1000);
|
|
79
|
+
if (origin) return { ok: true, origin, started: true };
|
|
80
|
+
const tail = logTail(logFile);
|
|
81
|
+
return { ok: false, message: `The dashboard exited at start (code ${exitCode})${tail ? `: ${tail}` : ""}.`, fix: `read ${logFile}, or run \`npx vfrey dashboard\` in the foreground to see the error.` };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const tail = logTail(logFile);
|
|
85
|
+
return { ok: false, message: `The dashboard did not answer within ${Math.round((options.timeoutMs ?? 15_000) / 1000)} s${tail ? `: ${tail}` : ""}.`, fix: `read ${logFile}, or run \`npx vfrey dashboard\` in the foreground to see the error.` };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Opens a URL in the default browser. Never throws; the URL is printed anyway. */
|
|
89
|
+
export function openBrowser(url: string, platform: NodeJS.Platform = process.platform): void {
|
|
90
|
+
const [cmd, args]: [string, string[]] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
91
|
+
try {
|
|
92
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
93
|
+
child.on("error", () => {});
|
|
94
|
+
child.unref();
|
|
95
|
+
} catch {
|
|
96
|
+
/* no browser: the printed URL is enough */
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Whether to skip the browser: --no-browser, VFREY_NO_BROWSER=1, or a test runner. */
|
|
101
|
+
export function browserDisabled(argv: string[], env: Env): boolean {
|
|
102
|
+
return argv.includes("--no-browser") || env.VFREY_NO_BROWSER === "1" || env.VITEST !== undefined || env.NODE_ENV === "test";
|
|
103
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { credentialsKey, decryptSecret } from "@vfrey/contracts";
|
|
2
|
+
import type { Store } from "../queue/store.js";
|
|
3
|
+
|
|
4
|
+
export const CHANNEL_PLATFORMS = ["youtube", "tiktok", "instagram", "facebook"] as const;
|
|
5
|
+
export type ChannelPlatform = (typeof CHANNEL_PLATFORMS)[number];
|
|
6
|
+
|
|
7
|
+
/** Where each platform's channel or page id already lives. YouTube has none, so its id is stored in the platform config. */
|
|
8
|
+
const ID_FIELD: Partial<Record<ChannelPlatform, string>> = {
|
|
9
|
+
tiktok: "tiktokOpenId",
|
|
10
|
+
instagram: "igUserId",
|
|
11
|
+
facebook: "fbPageId",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export interface ChannelInfo {
|
|
15
|
+
platform: ChannelPlatform;
|
|
16
|
+
enabled: boolean;
|
|
17
|
+
/** The id on file, or null when none is known yet. */
|
|
18
|
+
id: string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The channel or page id each platform of a series is known by: the TikTok open id, the Instagram user id and the Facebook
|
|
23
|
+
* page id come from the series' stored keys, the YouTube channel id from the platform's `channelId` setting.
|
|
24
|
+
* These are public identifiers, not tokens. An id that cannot be decrypted counts as unknown.
|
|
25
|
+
*/
|
|
26
|
+
export function channelInfo(store: Store, seriesId: string, rawKey: string | undefined): ChannelInfo[] {
|
|
27
|
+
let key: Buffer | null = null;
|
|
28
|
+
try {
|
|
29
|
+
key = credentialsKey(rawKey ?? "");
|
|
30
|
+
} catch {
|
|
31
|
+
key = null;
|
|
32
|
+
}
|
|
33
|
+
const platforms = store.db.prepare(`SELECT platform, enabled, config_json FROM desk_platforms WHERE series_id = ?`).all(seriesId) as Array<{
|
|
34
|
+
platform: string;
|
|
35
|
+
enabled: number;
|
|
36
|
+
config_json: string;
|
|
37
|
+
}>;
|
|
38
|
+
return CHANNEL_PLATFORMS.map((platform) => {
|
|
39
|
+
const row = platforms.find((item) => item.platform === platform);
|
|
40
|
+
let id: string | null = null;
|
|
41
|
+
const field = ID_FIELD[platform];
|
|
42
|
+
if (field && key) {
|
|
43
|
+
const secret = store.db
|
|
44
|
+
.prepare(`SELECT ciphertext FROM desk_secrets WHERE owner_kind = 'series' AND owner_id = ? AND field = ?`)
|
|
45
|
+
.get(seriesId, field) as { ciphertext: string } | undefined;
|
|
46
|
+
if (secret) {
|
|
47
|
+
try {
|
|
48
|
+
id = decryptSecret(secret.ciphertext, key).trim() || null;
|
|
49
|
+
} catch {
|
|
50
|
+
id = null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!id && row) {
|
|
55
|
+
try {
|
|
56
|
+
const config = JSON.parse(row.config_json) as { channelId?: unknown };
|
|
57
|
+
if (typeof config.channelId === "string" && config.channelId.trim()) id = config.channelId.trim();
|
|
58
|
+
} catch {
|
|
59
|
+
id = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { platform, enabled: Boolean(row && Number(row.enabled) === 1), id };
|
|
63
|
+
});
|
|
64
|
+
}
|