@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,26 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { TRANSITION_TYPES, DEFAULT_TRANSITION, DEFAULT_TRANSITION_SECONDS, type TransitionType } from "./runtime/transitions.js";
|
|
4
|
+
import { TRANSITIONS_DIR } from "./supply.js";
|
|
5
|
+
|
|
6
|
+
export interface TransitionIndex {
|
|
7
|
+
source: string;
|
|
8
|
+
commit: string;
|
|
9
|
+
runtime: { file: string; bytes: number };
|
|
10
|
+
default: string;
|
|
11
|
+
defaultSeconds: number;
|
|
12
|
+
transitions: TransitionType[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The transition types a scene can use, with where the runtime came from (src/scene/transitions/SOURCE.json). */
|
|
16
|
+
export async function transitionIndex(): Promise<TransitionIndex> {
|
|
17
|
+
const source = JSON.parse(await readFile(path.join(TRANSITIONS_DIR, "SOURCE.json"), "utf8")) as { source: string; commit: string; bytes: number; shaders: string[] };
|
|
18
|
+
const known = new Set(TRANSITION_TYPES.map((t) => t.name));
|
|
19
|
+
const shipped = new Set(source.shaders);
|
|
20
|
+
for (const name of source.shaders) if (!known.has(name)) throw new Error(`the transitions runtime has a shader, ${name}, that vfrey does not describe.`);
|
|
21
|
+
for (const t of TRANSITION_TYPES) if (t.gpu && !shipped.has(t.name)) throw new Error(`vfrey lists the transition ${t.name}, which the runtime does not have.`);
|
|
22
|
+
return { source: source.source, commit: source.commit, runtime: { file: "vfrey/transitions.js", bytes: source.bytes }, default: DEFAULT_TRANSITION, defaultSeconds: DEFAULT_TRANSITION_SECONDS, transitions: [...TRANSITION_TYPES] };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The text a Remotion scene gets when it asks for a shader transition. */
|
|
26
|
+
export const REMOTION_TRANSITIONS_FIX = "use an HTML scene (`npx vfrey scene init DIR`) with --engine vfrey-framey or hyperframes, or build the transition in React with Remotion's own interpolate().";
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { isMissing, readIndex, type IndexEntry } from "../project/index-file.js";
|
|
5
|
+
import { projectQueue, projectSeriesDue, type ProjectQueue, type SeriesDue } from "./due.js";
|
|
6
|
+
import type { FileLog } from "./log-file.js";
|
|
7
|
+
import { LOCK_BUSY_EXIT, liveHolder } from "./lock.js";
|
|
8
|
+
import { readState, writeState } from "./state.js";
|
|
9
|
+
|
|
10
|
+
export type Outcome = "ticked" | "not due" | "busy" | "missing" | "no series" | "error";
|
|
11
|
+
|
|
12
|
+
export interface ProjectResult {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
root: string;
|
|
16
|
+
outcome: Outcome;
|
|
17
|
+
detail?: string;
|
|
18
|
+
/** Jobs waiting for review in the project. */
|
|
19
|
+
waiting?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CycleReport {
|
|
23
|
+
at: string;
|
|
24
|
+
projects: ProjectResult[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface TickOutcome {
|
|
28
|
+
code: number;
|
|
29
|
+
output: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CycleDeps {
|
|
33
|
+
machineHome: string;
|
|
34
|
+
now: () => Date;
|
|
35
|
+
log: FileLog;
|
|
36
|
+
/** Series due state for a project root; null when the project has no database yet. */
|
|
37
|
+
due?: (root: string, now: Date) => SeriesDue[] | null;
|
|
38
|
+
/** The project's queue: jobs waiting for review, and pending jobs that can start now. Null when it has no database yet. */
|
|
39
|
+
queue?: (root: string, now: Date) => ProjectQueue | null;
|
|
40
|
+
/** Runs one project's tick. The default starts `vfrey desk tick` in the project. */
|
|
41
|
+
tickProject: (entry: IndexEntry) => Promise<TickOutcome>;
|
|
42
|
+
isMissing?: (entry: IndexEntry) => boolean;
|
|
43
|
+
isAlive?: (pid: number) => boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** One pass over the machine index. Never throws for a single project's trouble. */
|
|
47
|
+
export async function runCycle(deps: CycleDeps): Promise<CycleReport> {
|
|
48
|
+
const now = deps.now();
|
|
49
|
+
const entries = readIndex(deps.machineHome);
|
|
50
|
+
const state = readState(deps.machineHome);
|
|
51
|
+
const results: ProjectResult[] = [];
|
|
52
|
+
const stopped = (entry: IndexEntry, outcome: Outcome, detail?: string, waiting?: number) =>
|
|
53
|
+
results.push({ id: entry.id, name: entry.name, root: entry.root, outcome, ...(detail ? { detail } : {}), ...(waiting ? { waiting } : {}) });
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
if ((deps.isMissing ?? isMissing)(entry)) {
|
|
56
|
+
deps.log.write("warn", "project skipped: folder or vfrey.json is gone", { project: entry.name, root: entry.root });
|
|
57
|
+
stopped(entry, "missing", "folder or vfrey.json is gone");
|
|
58
|
+
state.projects[entry.id] = { lastTick: state.projects[entry.id]?.lastTick ?? null, outcome: "missing" };
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
let series: SeriesDue[] | null;
|
|
62
|
+
try {
|
|
63
|
+
series = (deps.due ?? projectSeriesDue)(entry.root, now);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
66
|
+
deps.log.write("error", "project skipped: cannot read its series", { project: entry.name, message });
|
|
67
|
+
stopped(entry, "error", message);
|
|
68
|
+
state.projects[entry.id] = { lastTick: state.projects[entry.id]?.lastTick ?? null, outcome: "error", detail: message };
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const previous = state.projects[entry.id]?.lastTick ?? null;
|
|
72
|
+
let queue: ProjectQueue | null = null;
|
|
73
|
+
try {
|
|
74
|
+
queue = (deps.queue ?? projectQueue)(entry.root, now);
|
|
75
|
+
} catch {
|
|
76
|
+
queue = null;
|
|
77
|
+
}
|
|
78
|
+
let waiting = queue?.waiting ?? 0;
|
|
79
|
+
if (waiting > 0) deps.log.write("info", "jobs wait for review", { project: entry.name, waiting });
|
|
80
|
+
if (!series || series.length === 0) {
|
|
81
|
+
stopped(entry, "no series", undefined, waiting);
|
|
82
|
+
state.projects[entry.id] = { lastTick: previous, outcome: "no series", waiting };
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
// An approved job is pending again and has no cadence of its own: the tick that renders it must not wait for a due series.
|
|
86
|
+
if (!series.some((item) => item.due && !item.paused) && !(queue && queue.runnable > 0)) {
|
|
87
|
+
stopped(entry, "not due", undefined, waiting);
|
|
88
|
+
state.projects[entry.id] = { lastTick: previous, outcome: "not due", waiting };
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const holder = liveHolder(path.join(entry.root, ".vfrey"), deps.isAlive);
|
|
92
|
+
if (holder) {
|
|
93
|
+
deps.log.write("info", "project busy: another tick holds the lock", { project: entry.name, pid: holder.pid });
|
|
94
|
+
stopped(entry, "busy", `pid ${holder.pid}`, waiting);
|
|
95
|
+
state.projects[entry.id] = { lastTick: previous, outcome: "busy", waiting };
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
deps.log.write("info", "ticking project", { project: entry.name, root: entry.root });
|
|
99
|
+
let outcome: TickOutcome;
|
|
100
|
+
try {
|
|
101
|
+
outcome = await deps.tickProject(entry);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
outcome = { code: 1, output: error instanceof Error ? error.message : String(error) };
|
|
104
|
+
}
|
|
105
|
+
if (outcome.output.trim()) deps.log.raw(entry.name, outcome.output);
|
|
106
|
+
// The tick may have made a job that now waits for review.
|
|
107
|
+
try {
|
|
108
|
+
waiting = (deps.queue ?? projectQueue)(entry.root, deps.now())?.waiting ?? waiting;
|
|
109
|
+
} catch {
|
|
110
|
+
/* keep the count from before the tick */
|
|
111
|
+
}
|
|
112
|
+
if (outcome.code === 0) {
|
|
113
|
+
deps.log.write("info", "project ticked", { project: entry.name });
|
|
114
|
+
stopped(entry, "ticked", undefined, waiting);
|
|
115
|
+
state.projects[entry.id] = { lastTick: deps.now().toISOString(), outcome: "ticked", waiting };
|
|
116
|
+
} else if (outcome.code === LOCK_BUSY_EXIT) {
|
|
117
|
+
stopped(entry, "busy", undefined, waiting);
|
|
118
|
+
state.projects[entry.id] = { lastTick: previous, outcome: "busy", waiting };
|
|
119
|
+
} else {
|
|
120
|
+
deps.log.write("error", "project tick failed", { project: entry.name, code: outcome.code });
|
|
121
|
+
stopped(entry, "error", `exit ${outcome.code}`, waiting);
|
|
122
|
+
state.projects[entry.id] = { lastTick: previous, outcome: "error", detail: `exit ${outcome.code}`, waiting };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const report = { at: now.toISOString(), projects: results };
|
|
126
|
+
writeState(deps.machineHome, { ...state, pid: process.pid, lastCycle: report.at });
|
|
127
|
+
deps.log.write("info", "cycle complete", { projects: results.length, ticked: results.filter((r) => r.outcome === "ticked").length, waiting: results.reduce((sum, r) => sum + (r.waiting ?? 0), 0), skipped: results.filter((r) => r.outcome === "missing").length });
|
|
128
|
+
return report;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export const VFREY_BIN = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../bin/vfrey.mjs");
|
|
132
|
+
|
|
133
|
+
/** The real project tick: `vfrey desk tick` as a child in the project, so its .env and data folder are its own. */
|
|
134
|
+
export function spawnProjectTick(machineHome: string, env: Record<string, string | undefined>, extraArgs: string[] = [], bin: string = VFREY_BIN) {
|
|
135
|
+
const children = new Set<ReturnType<typeof spawn>>();
|
|
136
|
+
const run = (entry: IndexEntry): Promise<TickOutcome> =>
|
|
137
|
+
new Promise((resolve) => {
|
|
138
|
+
const childEnv: Record<string, string | undefined> = { ...env, VFREY_MACHINE_HOME: machineHome };
|
|
139
|
+
delete childEnv.VFREY_HOME;
|
|
140
|
+
delete childEnv.DATA_DIR;
|
|
141
|
+
const child = spawn(process.execPath, [bin, "desk", "tick", ...extraArgs], { cwd: entry.root, env: childEnv as NodeJS.ProcessEnv, stdio: ["ignore", "pipe", "pipe"] });
|
|
142
|
+
children.add(child);
|
|
143
|
+
let output = "";
|
|
144
|
+
child.stdout!.on("data", (chunk: Buffer) => (output += chunk.toString("utf8")));
|
|
145
|
+
child.stderr!.on("data", (chunk: Buffer) => (output += chunk.toString("utf8")));
|
|
146
|
+
child.on("error", (error) => resolve({ code: 1, output: error.message }));
|
|
147
|
+
child.on("close", (code) => {
|
|
148
|
+
children.delete(child);
|
|
149
|
+
resolve({ code: code ?? 1, output });
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
return { run, stopChildren: (signal: NodeJS.Signals) => children.forEach((child) => child.kill(signal)) };
|
|
153
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, statSync, openSync, readSync, closeSync, writeFileSync } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { resolveHome } from "../home.js";
|
|
6
|
+
import { readIndex, isMissing } from "../project/index-file.js";
|
|
7
|
+
import { shortHome } from "../desk/state.js";
|
|
8
|
+
import { VFREY_BIN } from "./all-projects.js";
|
|
9
|
+
import { projectQueue, projectSeriesDue, type SeriesDue } from "./due.js";
|
|
10
|
+
import { liveHolder } from "./lock.js";
|
|
11
|
+
import { logsDir, workerLogPath } from "./log-file.js";
|
|
12
|
+
import { readState } from "./state.js";
|
|
13
|
+
import { LAUNCHD_LABEL, SYSTEMD_UNIT, plistPath, renderPlist, renderUnit, serviceEnv, unitPath, type UnitInput } from "./units.js";
|
|
14
|
+
|
|
15
|
+
export interface RunResult {
|
|
16
|
+
code: number;
|
|
17
|
+
stdout: string;
|
|
18
|
+
stderr: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Everything the service commands touch outside this process. Tests inject all of it. */
|
|
22
|
+
export interface ServiceDeps {
|
|
23
|
+
platform: NodeJS.Platform;
|
|
24
|
+
homedir: string;
|
|
25
|
+
uid: number;
|
|
26
|
+
node: string;
|
|
27
|
+
bin: string;
|
|
28
|
+
now: () => Date;
|
|
29
|
+
/** launchctl or systemctl. Never the real one in tests. */
|
|
30
|
+
run: (command: string, args: string[]) => RunResult;
|
|
31
|
+
/** Writes the plist or unit. */
|
|
32
|
+
writeFile: (file: string, content: string) => void;
|
|
33
|
+
removeFile: (file: string) => void;
|
|
34
|
+
readFile: (file: string) => string | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ServiceIo {
|
|
38
|
+
env: Record<string, string | undefined>;
|
|
39
|
+
cwd: string;
|
|
40
|
+
stdout: (text: string) => void;
|
|
41
|
+
stderr: (text: string) => void;
|
|
42
|
+
/** Stops `logs --follow`. */
|
|
43
|
+
signal?: AbortSignal;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function realDeps(): ServiceDeps {
|
|
47
|
+
return {
|
|
48
|
+
platform: process.platform,
|
|
49
|
+
homedir: os.homedir(),
|
|
50
|
+
uid: process.getuid?.() ?? 0,
|
|
51
|
+
node: process.execPath,
|
|
52
|
+
bin: VFREY_BIN,
|
|
53
|
+
now: () => new Date(),
|
|
54
|
+
run: (command, args) => {
|
|
55
|
+
const result = spawnSync(command, args, { encoding: "utf8" });
|
|
56
|
+
return { code: result.status ?? 1, stdout: result.stdout ?? "", stderr: result.stderr ?? (result.error ? result.error.message : "") };
|
|
57
|
+
},
|
|
58
|
+
writeFile: (file, content) => {
|
|
59
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
60
|
+
writeFileSync(file, content);
|
|
61
|
+
},
|
|
62
|
+
removeFile: (file) => rmSync(file, { force: true }),
|
|
63
|
+
readFile: (file) => (existsSync(file) ? readFileSync(file, "utf8") : null),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const fail = (io: ServiceIo, message: string, fix: string): number => {
|
|
68
|
+
io.stderr(`✗ ${message}\nFix: ${fix}\n`);
|
|
69
|
+
return 1;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const SUBCOMMANDS = ["install", "uninstall", "status", "logs"];
|
|
73
|
+
|
|
74
|
+
type Kind = "launchd" | "systemd";
|
|
75
|
+
const kindOf = (platform: NodeJS.Platform): Kind | null => (platform === "darwin" ? "launchd" : platform === "linux" ? "systemd" : null);
|
|
76
|
+
|
|
77
|
+
function unitInput(deps: ServiceDeps, io: ServiceIo, machineHome: string): UnitInput {
|
|
78
|
+
return {
|
|
79
|
+
node: deps.node,
|
|
80
|
+
bin: deps.bin,
|
|
81
|
+
machineHome,
|
|
82
|
+
env: { ...serviceEnv({ HOME: deps.homedir, ...io.env }) },
|
|
83
|
+
// launchd and systemd hold this file open, so it cannot be rotated. The worker writes worker.log itself.
|
|
84
|
+
logFile: path.join(logsDir(machineHome), "service.log"),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface ServiceState {
|
|
89
|
+
installed: boolean;
|
|
90
|
+
running: boolean;
|
|
91
|
+
pid: number | null;
|
|
92
|
+
file: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function serviceState(deps: ServiceDeps, kind: Kind): ServiceState {
|
|
96
|
+
if (kind === "launchd") {
|
|
97
|
+
const file = plistPath(deps.homedir);
|
|
98
|
+
const printed = deps.run("launchctl", ["print", `gui/${deps.uid}/${LAUNCHD_LABEL}`]);
|
|
99
|
+
const pid = /\bpid = (\d+)/.exec(printed.stdout)?.[1];
|
|
100
|
+
return { installed: deps.readFile(file) !== null, running: printed.code === 0 && /state = running/.test(printed.stdout), pid: pid ? Number(pid) : null, file };
|
|
101
|
+
}
|
|
102
|
+
const file = unitPath(deps.homedir);
|
|
103
|
+
const active = deps.run("systemctl", ["--user", "is-active", SYSTEMD_UNIT]);
|
|
104
|
+
const main = deps.run("systemctl", ["--user", "show", "-p", "MainPID", "--value", SYSTEMD_UNIT]);
|
|
105
|
+
const pid = Number(main.stdout.trim());
|
|
106
|
+
return { installed: deps.readFile(file) !== null, running: active.code === 0 && active.stdout.trim() === "active", pid: pid > 0 ? pid : null, file };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function install(io: ServiceIo, deps: ServiceDeps, kind: Kind): number {
|
|
110
|
+
const home = resolveHome(io.env, deps.homedir, io.cwd);
|
|
111
|
+
const machineHome = home.machineHome;
|
|
112
|
+
const input = unitInput(deps, io, machineHome);
|
|
113
|
+
const file = kind === "launchd" ? plistPath(deps.homedir) : unitPath(deps.homedir);
|
|
114
|
+
const content = kind === "launchd" ? renderPlist(input) : renderUnit(input);
|
|
115
|
+
const same = deps.readFile(file) === content;
|
|
116
|
+
const short = (item: string) => shortHome(item, deps.homedir);
|
|
117
|
+
if (!same) deps.writeFile(file, content);
|
|
118
|
+
io.stdout(same ? `Unchanged ${short(file)}\n` : `Wrote ${short(file)}\n`);
|
|
119
|
+
io.stdout(` runs: ${short(deps.node)} ${short(deps.bin)} worker --all-projects\n log: ${short(workerLogPath(machineHome))}\n`);
|
|
120
|
+
if (kind === "launchd") {
|
|
121
|
+
const domain = `gui/${deps.uid}`;
|
|
122
|
+
deps.run("launchctl", ["bootout", `${domain}/${LAUNCHD_LABEL}`]);
|
|
123
|
+
const loaded = deps.run("launchctl", ["bootstrap", domain, file]);
|
|
124
|
+
if (loaded.code !== 0) return fail(io, `launchctl could not load the agent: ${loaded.stderr.trim() || `exit ${loaded.code}`}`, `run \`launchctl bootstrap ${domain} ${short(file)}\` and read its error.`);
|
|
125
|
+
} else {
|
|
126
|
+
deps.run("systemctl", ["--user", "daemon-reload"]);
|
|
127
|
+
const enabled = deps.run("systemctl", ["--user", "enable", "--now", SYSTEMD_UNIT]);
|
|
128
|
+
if (enabled.code !== 0) return fail(io, `systemctl could not start the worker: ${enabled.stderr.trim() || `exit ${enabled.code}`}`, "run `systemctl --user enable --now vfrey-worker.service` and read its error; `loginctl enable-linger $USER` keeps it running after logout.");
|
|
129
|
+
}
|
|
130
|
+
io.stdout("The worker now runs in the background and starts at login. Check it: npx vfrey service status\n");
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function uninstall(io: ServiceIo, deps: ServiceDeps, kind: Kind): number {
|
|
135
|
+
const state = serviceState(deps, kind);
|
|
136
|
+
if (!state.installed && !state.running) {
|
|
137
|
+
io.stdout("The background worker is not installed. Nothing to remove.\n");
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
if (kind === "launchd") deps.run("launchctl", ["bootout", `gui/${deps.uid}/${LAUNCHD_LABEL}`]);
|
|
141
|
+
else deps.run("systemctl", ["--user", "disable", "--now", SYSTEMD_UNIT]);
|
|
142
|
+
deps.removeFile(state.file);
|
|
143
|
+
if (kind === "systemd") deps.run("systemctl", ["--user", "daemon-reload"]);
|
|
144
|
+
io.stdout(`Stopped the background worker and removed ${shortHome(state.file, deps.homedir)}\n`);
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const when = (iso: string | null): string => (iso ? iso.replace("T", " ").replace(/\.\d+Z$/, "Z") : "never");
|
|
149
|
+
|
|
150
|
+
/** The soonest next-due time over a project's unpaused series. */
|
|
151
|
+
export function projectNextDue(series: SeriesDue[] | null): string {
|
|
152
|
+
if (!series || series.length === 0) return "no series";
|
|
153
|
+
const live = series.filter((item) => !item.paused);
|
|
154
|
+
if (live.length === 0) return "paused";
|
|
155
|
+
const times = live.map((item) => item.nextDue).filter((item): item is Date => item !== null);
|
|
156
|
+
if (times.length === 0) return "not within 72 h";
|
|
157
|
+
return times.reduce((a, b) => (a < b ? a : b)).toISOString();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function status(io: ServiceIo, deps: ServiceDeps, kind: Kind | null, json: boolean): number {
|
|
161
|
+
const home = resolveHome(io.env, deps.homedir, io.cwd);
|
|
162
|
+
const state = kind ? serviceState(deps, kind) : null;
|
|
163
|
+
const worker = readState(home.machineHome);
|
|
164
|
+
const now = deps.now();
|
|
165
|
+
const projects = readIndex(home.machineHome).map((entry) => {
|
|
166
|
+
if (isMissing(entry)) return { name: entry.name, root: entry.root, missing: true, lastTick: worker.projects[entry.id]?.lastTick ?? null, nextDue: "missing", busy: false, waitingForReview: 0 };
|
|
167
|
+
let series: SeriesDue[] | null = null;
|
|
168
|
+
try {
|
|
169
|
+
series = projectSeriesDue(entry.root, now);
|
|
170
|
+
} catch {
|
|
171
|
+
series = null;
|
|
172
|
+
}
|
|
173
|
+
let waitingForReview = 0;
|
|
174
|
+
try {
|
|
175
|
+
waitingForReview = projectQueue(entry.root, now)?.waiting ?? 0;
|
|
176
|
+
} catch {
|
|
177
|
+
waitingForReview = 0;
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
name: entry.name,
|
|
181
|
+
root: entry.root,
|
|
182
|
+
missing: false,
|
|
183
|
+
lastTick: worker.projects[entry.id]?.lastTick ?? null,
|
|
184
|
+
nextDue: projectNextDue(series),
|
|
185
|
+
busy: liveHolder(path.join(entry.root, ".vfrey")) !== null,
|
|
186
|
+
waitingForReview,
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
const label = !state ? "unsupported on this system" : state.running ? `running${state.pid ? ` (pid ${state.pid})` : ""}` : state.installed ? "installed, not running" : "not installed";
|
|
190
|
+
if (json) {
|
|
191
|
+
io.stdout(JSON.stringify({ service: { kind, state: label, installed: state?.installed ?? false, running: state?.running ?? false, file: state?.file ?? null }, lastCycle: worker.lastCycle, log: workerLogPath(home.machineHome), projects }, null, 2) + "\n");
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
io.stdout(`Service: ${label}${state ? ` (${shortHome(state.file, deps.homedir)})` : ""}\n`);
|
|
195
|
+
if (!state || !state.installed) io.stdout("Install it: npx vfrey service install\n");
|
|
196
|
+
io.stdout(`Last cycle: ${when(worker.lastCycle)}\n`);
|
|
197
|
+
if (projects.length === 0) io.stdout("Projects: none yet. Start one: npm create vfrey@latest my-channel\n");
|
|
198
|
+
for (const p of projects) {
|
|
199
|
+
io.stdout(` ${p.name} ${shortHome(p.root, deps.homedir)} ${p.missing ? "missing" : `last tick ${when(p.lastTick)} next due ${p.nextDue}${p.busy ? " ticking now" : ""}${p.waitingForReview ? ` ${p.waitingForReview} waiting for review` : ""}`}\n`);
|
|
200
|
+
}
|
|
201
|
+
return 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function lastLines(file: string, count: number): string {
|
|
205
|
+
const text = readFileSync(file, "utf8").split("\n");
|
|
206
|
+
if (text.at(-1) === "") text.pop();
|
|
207
|
+
return text.slice(-count).join("\n") + "\n";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function logs(io: ServiceIo, deps: ServiceDeps, argv: string[]): Promise<number> {
|
|
211
|
+
const file = workerLogPath(resolveHome(io.env, deps.homedir, io.cwd).machineHome);
|
|
212
|
+
if (!existsSync(file)) return fail(io, `There is no worker log yet at ${shortHome(file, deps.homedir)}.`, "install the worker with `npx vfrey service install`, or run `npx vfrey worker --all-projects`.");
|
|
213
|
+
io.stdout(lastLines(file, 50));
|
|
214
|
+
if (!argv.includes("--follow") && !argv.includes("-f")) return 0;
|
|
215
|
+
let offset = statSync(file).size;
|
|
216
|
+
await new Promise<void>((resolve) => {
|
|
217
|
+
const timer = setInterval(() => {
|
|
218
|
+
try {
|
|
219
|
+
const size = statSync(file).size;
|
|
220
|
+
if (size < offset) offset = 0; // rotated
|
|
221
|
+
if (size > offset) {
|
|
222
|
+
const fd = openSync(file, "r");
|
|
223
|
+
const buffer = Buffer.alloc(size - offset);
|
|
224
|
+
readSync(fd, buffer, 0, buffer.length, offset);
|
|
225
|
+
closeSync(fd);
|
|
226
|
+
offset = size;
|
|
227
|
+
io.stdout(buffer.toString("utf8"));
|
|
228
|
+
}
|
|
229
|
+
} catch {
|
|
230
|
+
/* the file is being rotated; try again */
|
|
231
|
+
}
|
|
232
|
+
}, 300);
|
|
233
|
+
const done = () => {
|
|
234
|
+
clearInterval(timer);
|
|
235
|
+
resolve();
|
|
236
|
+
};
|
|
237
|
+
io.signal?.addEventListener("abort", done, { once: true });
|
|
238
|
+
process.once("SIGINT", done);
|
|
239
|
+
});
|
|
240
|
+
return 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** `vfrey service install | uninstall | status | logs`. */
|
|
244
|
+
export async function runService(argv: string[], io: ServiceIo, deps: ServiceDeps = realDeps()): Promise<number> {
|
|
245
|
+
const sub = argv[0];
|
|
246
|
+
if (!sub || !SUBCOMMANDS.includes(sub)) return fail(io, `Unknown service command "${sub ?? ""}".`, `use one of: ${SUBCOMMANDS.join(", ")}.`);
|
|
247
|
+
if (sub === "logs") return logs(io, deps, argv);
|
|
248
|
+
const kind = kindOf(deps.platform);
|
|
249
|
+
if (!kind) {
|
|
250
|
+
if (sub === "status") return status(io, deps, null, argv.includes("--json"));
|
|
251
|
+
return fail(io, `The background service is supported on macOS and Linux, not ${deps.platform}.`, "run `npx vfrey worker --all-projects` in a terminal and leave it open.");
|
|
252
|
+
}
|
|
253
|
+
if (sub === "install") return install(io, deps, kind);
|
|
254
|
+
if (sub === "uninstall") return uninstall(io, deps, kind);
|
|
255
|
+
return status(io, deps, kind, argv.includes("--json"));
|
|
256
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { cadenceDecision, localDayKey } from "@vfrey/contracts";
|
|
4
|
+
import { validateCadence } from "../config.js";
|
|
5
|
+
import { applyDeskAccount } from "../desk/overlay.js";
|
|
6
|
+
import { deskAccounts } from "../desk/series.js";
|
|
7
|
+
import { Store } from "../queue/store.js";
|
|
8
|
+
|
|
9
|
+
export interface SeriesDue {
|
|
10
|
+
id: string;
|
|
11
|
+
paused: boolean;
|
|
12
|
+
due: boolean;
|
|
13
|
+
reason: string;
|
|
14
|
+
/** When the series is next due (now when due). Null when paused, or not within the next 72 hours. */
|
|
15
|
+
nextDue: Date | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The first minute within 72 hours at which the cadence lets a video start. */
|
|
19
|
+
export function nextDueAt(cadence: Parameters<typeof cadenceDecision>[0]["cadence"], times: Date[], now: Date): Date | null {
|
|
20
|
+
for (let minute = 0; minute <= 72 * 60; minute += 1) {
|
|
21
|
+
const at = new Date(now.getTime() + minute * 60_000);
|
|
22
|
+
const day = localDayKey(at, cadence.timezone);
|
|
23
|
+
const decision = cadenceDecision({
|
|
24
|
+
cadence,
|
|
25
|
+
now: at,
|
|
26
|
+
lastPublishedAt: times[0] ?? null,
|
|
27
|
+
postsOnLocalDay: times.filter((time) => localDayKey(time, cadence.timezone) === day).length,
|
|
28
|
+
});
|
|
29
|
+
if (decision.due) return at;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Every series of a project and whether it is due. Reads the project's own database; null when it has none yet. */
|
|
35
|
+
export function projectSeriesDue(root: string, now: Date): SeriesDue[] | null {
|
|
36
|
+
const file = path.join(root, ".vfrey", "pipeline.sqlite");
|
|
37
|
+
if (!existsSync(file)) return null;
|
|
38
|
+
const store = new Store(file);
|
|
39
|
+
try {
|
|
40
|
+
return deskAccounts(store).map((base): SeriesDue => {
|
|
41
|
+
const overlay = store.deskOverlay(base.id);
|
|
42
|
+
const account = overlay ? applyDeskAccount(base, overlay) : base;
|
|
43
|
+
if (store.seriesPaused(account.id)) return { id: account.id, paused: true, due: false, reason: "paused", nextDue: null };
|
|
44
|
+
const fault = validateCadence(account.cadence);
|
|
45
|
+
if (fault) return { id: account.id, paused: false, due: false, reason: `invalid_cadence: ${fault}`, nextDue: null };
|
|
46
|
+
const times = store.videoTimes(account.id);
|
|
47
|
+
const day = localDayKey(now, account.cadence.timezone);
|
|
48
|
+
const decision = cadenceDecision({
|
|
49
|
+
cadence: account.cadence,
|
|
50
|
+
now,
|
|
51
|
+
lastPublishedAt: times[0] ?? null,
|
|
52
|
+
postsOnLocalDay: times.filter((time) => localDayKey(time, account.cadence.timezone) === day).length,
|
|
53
|
+
});
|
|
54
|
+
return { id: account.id, paused: false, due: decision.due, reason: decision.reason, nextDue: decision.due ? now : nextDueAt(account.cadence, times, now) };
|
|
55
|
+
});
|
|
56
|
+
} finally {
|
|
57
|
+
store.close();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ProjectQueue {
|
|
62
|
+
/** Jobs that wait for a person to approve or reject them (spec 015 D). They hold no worker. */
|
|
63
|
+
waiting: number;
|
|
64
|
+
/** Pending jobs that can start now, such as one that was just approved. A tick is worth running for them even when no series is due. */
|
|
65
|
+
runnable: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** What a project's queue holds besides the cadence. Null when the project has no database yet. */
|
|
69
|
+
export function projectQueue(root: string, now: Date): ProjectQueue | null {
|
|
70
|
+
const file = path.join(root, ".vfrey", "pipeline.sqlite");
|
|
71
|
+
if (!existsSync(file)) return null;
|
|
72
|
+
const store = new Store(file);
|
|
73
|
+
try {
|
|
74
|
+
const count = (sql: string, ...args: string[]) => Number((store.db.prepare(sql).get(...args) as { n: number }).n);
|
|
75
|
+
return {
|
|
76
|
+
waiting: count(`SELECT COUNT(*) AS n FROM jobs WHERE status = 'awaiting_review'`),
|
|
77
|
+
runnable: count(`SELECT COUNT(*) AS n FROM jobs WHERE kind = 'pipeline' AND status = 'pending' AND run_after <= ?`, now.toISOString()),
|
|
78
|
+
};
|
|
79
|
+
} finally {
|
|
80
|
+
store.close();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const LOCK_FILE = "worker.lock";
|
|
5
|
+
|
|
6
|
+
export interface LockHolder {
|
|
7
|
+
pid: number;
|
|
8
|
+
started: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type LockResult = { ok: true; release: () => void } | { ok: false; holder: LockHolder };
|
|
12
|
+
|
|
13
|
+
export interface LockOptions {
|
|
14
|
+
pid?: number;
|
|
15
|
+
now?: () => Date;
|
|
16
|
+
/** Whether a process is alive. Injected in tests. */
|
|
17
|
+
isAlive?: (pid: number) => boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function pidAlive(pid: number): boolean {
|
|
21
|
+
try {
|
|
22
|
+
process.kill(pid, 0);
|
|
23
|
+
return true;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
// EPERM means the process exists but belongs to someone else.
|
|
26
|
+
return (error as NodeJS.ErrnoException).code === "EPERM";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function lockPath(dataDir: string): string {
|
|
31
|
+
return path.join(dataDir, LOCK_FILE);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The holder written in the lock file, or null when it is missing or unreadable. */
|
|
35
|
+
export function readLock(dataDir: string): LockHolder | null {
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(readFileSync(lockPath(dataDir), "utf8")) as Partial<LockHolder>;
|
|
38
|
+
if (typeof parsed.pid === "number" && typeof parsed.started === "string") return { pid: parsed.pid, started: parsed.started };
|
|
39
|
+
return null;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The holder when a live process holds the lock; null when free or stale. */
|
|
46
|
+
export function liveHolder(dataDir: string, isAlive: (pid: number) => boolean = pidAlive): LockHolder | null {
|
|
47
|
+
const holder = readLock(dataDir);
|
|
48
|
+
return holder && isAlive(holder.pid) ? holder : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Takes `<dataDir>/worker.lock` for this process. A lock whose process is gone (or whose file is unreadable)
|
|
53
|
+
* is stale and is taken over. The same lock is used by the worker, `tick`, and `desk tick`, so they never run
|
|
54
|
+
* the same project at once.
|
|
55
|
+
*/
|
|
56
|
+
export function acquireLock(dataDir: string, options: LockOptions = {}): LockResult {
|
|
57
|
+
const pid = options.pid ?? process.pid;
|
|
58
|
+
const isAlive = options.isAlive ?? pidAlive;
|
|
59
|
+
const file = lockPath(dataDir);
|
|
60
|
+
const body = JSON.stringify({ pid, started: (options.now?.() ?? new Date()).toISOString() }) + "\n";
|
|
61
|
+
mkdirSync(dataDir, { recursive: true });
|
|
62
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
63
|
+
try {
|
|
64
|
+
writeFileSync(file, body, { flag: "wx" });
|
|
65
|
+
let released = false;
|
|
66
|
+
const release = () => {
|
|
67
|
+
if (released) return;
|
|
68
|
+
released = true;
|
|
69
|
+
// Only remove a lock that is still ours.
|
|
70
|
+
if (readLock(dataDir)?.pid === pid) rmSync(file, { force: true });
|
|
71
|
+
};
|
|
72
|
+
return { ok: true, release };
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
75
|
+
}
|
|
76
|
+
const holder = readLock(dataDir);
|
|
77
|
+
if (holder && holder.pid !== pid && isAlive(holder.pid)) return { ok: false, holder };
|
|
78
|
+
// Stale: the holder died, or the file is not a lock. Remove it and try again.
|
|
79
|
+
rmSync(file, { force: true });
|
|
80
|
+
}
|
|
81
|
+
const holder = readLock(dataDir);
|
|
82
|
+
return { ok: false, holder: holder ?? { pid: 0, started: "unknown" } };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Takes the lock and drops it when this process exits, however it exits. */
|
|
86
|
+
export function acquireLockForProcess(dataDir: string, options: LockOptions = {}): LockResult {
|
|
87
|
+
const result = acquireLock(dataDir, options);
|
|
88
|
+
if (result.ok) {
|
|
89
|
+
process.once("exit", result.release);
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const LOCK_BUSY_EXIT = 75;
|
|
95
|
+
|
|
96
|
+
export function busyMessage(holder: LockHolder): { message: string; fix: string } {
|
|
97
|
+
return {
|
|
98
|
+
message: `Another vfrey tick is already running for this project (pid ${holder.pid}, since ${holder.started}).`,
|
|
99
|
+
fix: "wait for it to finish; the background worker and a manual tick never run the same project at once.",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, renameSync, rmSync, statSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { redact } from "@vfrey/contracts";
|
|
4
|
+
|
|
5
|
+
export const MAX_LOG_BYTES = 5 * 1024 * 1024;
|
|
6
|
+
export const KEEP_LOGS = 3;
|
|
7
|
+
|
|
8
|
+
export const logsDir = (machineHome: string): string => path.join(machineHome, "logs");
|
|
9
|
+
export const workerLogPath = (machineHome: string): string => path.join(logsDir(machineHome), "worker.log");
|
|
10
|
+
|
|
11
|
+
/** worker.log -> worker.log.1 -> .2 -> .3; the oldest is dropped. */
|
|
12
|
+
export function rotate(file: string, keep: number = KEEP_LOGS): void {
|
|
13
|
+
rmSync(`${file}.${keep}`, { force: true });
|
|
14
|
+
for (let n = keep - 1; n >= 1; n -= 1) {
|
|
15
|
+
if (existsSync(`${file}.${n}`)) renameSync(`${file}.${n}`, `${file}.${n + 1}`);
|
|
16
|
+
}
|
|
17
|
+
if (existsSync(file)) renameSync(file, `${file}.1`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface FileLog {
|
|
21
|
+
write(level: string, message: string, fields?: Record<string, unknown>): void;
|
|
22
|
+
/** Appends raw text (a child's output), line by line, tagged. */
|
|
23
|
+
raw(tag: string, text: string): void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A JSON-lines log with size rotation. Secrets are redacted. Never throws: a log must not stop the worker. */
|
|
27
|
+
export function createFileLog(file: string, options: { maxBytes?: number; keep?: number; now?: () => Date; echo?: (line: string) => void } = {}): FileLog {
|
|
28
|
+
const maxBytes = options.maxBytes ?? MAX_LOG_BYTES;
|
|
29
|
+
const append = (line: string) => {
|
|
30
|
+
try {
|
|
31
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
32
|
+
let size = 0;
|
|
33
|
+
try {
|
|
34
|
+
size = statSync(file).size;
|
|
35
|
+
} catch {
|
|
36
|
+
/* first write */
|
|
37
|
+
}
|
|
38
|
+
if (size > 0 && size + Buffer.byteLength(line) > maxBytes) rotate(file, options.keep ?? KEEP_LOGS);
|
|
39
|
+
appendFileSync(file, line);
|
|
40
|
+
options.echo?.(line);
|
|
41
|
+
} catch {
|
|
42
|
+
/* ignore */
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const ts = () => (options.now?.() ?? new Date()).toISOString();
|
|
46
|
+
return {
|
|
47
|
+
write(level, message, fields = {}) {
|
|
48
|
+
append(redact(JSON.stringify({ ts: ts(), level, message, ...fields })) + "\n");
|
|
49
|
+
},
|
|
50
|
+
raw(tag, text) {
|
|
51
|
+
for (const line of text.split("\n")) {
|
|
52
|
+
if (line.trim()) append(redact(JSON.stringify({ ts: ts(), level: "info", message: line, source: tag })) + "\n");
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|