@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,282 @@
|
|
|
1
|
+
// The one vfrey API client. Every paid command uses it: it adds the token, maps failures to the CLI's ✗ and Fix: lines,
|
|
2
|
+
// and never prints the token.
|
|
3
|
+
import {
|
|
4
|
+
CLI_CLIENT_ID,
|
|
5
|
+
isApiError,
|
|
6
|
+
type ApiError,
|
|
7
|
+
type DeviceCodeResponse,
|
|
8
|
+
type DeviceTokenErrorCode,
|
|
9
|
+
type DeviceTokenResponse,
|
|
10
|
+
type MeResponse,
|
|
11
|
+
type NicheRequestBody,
|
|
12
|
+
type OrgsResponse,
|
|
13
|
+
type OrgSwitchResponse,
|
|
14
|
+
type NicheRequestResponse,
|
|
15
|
+
type NichesResponse,
|
|
16
|
+
type TopicsResponse,
|
|
17
|
+
type CreatorAlertsResponse,
|
|
18
|
+
type CreatorProfileView,
|
|
19
|
+
type CreatorSearchResponse,
|
|
20
|
+
type MailDraftBody,
|
|
21
|
+
type MailDraftPatch,
|
|
22
|
+
type MailDraftView,
|
|
23
|
+
type MailIdentityBody,
|
|
24
|
+
type MailIdentityView,
|
|
25
|
+
type WatchlistItemView,
|
|
26
|
+
type WatchlistView,
|
|
27
|
+
} from "./types.js";
|
|
28
|
+
|
|
29
|
+
export type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
30
|
+
|
|
31
|
+
/** A refusal the CLI prints as `✗ <message>` and `Fix: <fix>`. */
|
|
32
|
+
export class ApiFailure extends Error {
|
|
33
|
+
constructor(message: string, readonly fix: string, readonly status?: number, readonly code?: string) {
|
|
34
|
+
super(message);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ClientOptions {
|
|
39
|
+
baseUrl: string;
|
|
40
|
+
token?: string | null;
|
|
41
|
+
fetch?: FetchLike;
|
|
42
|
+
timeoutMs?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const TOKEN_SHAPE = /vfr_[A-Za-z0-9_-]+/g;
|
|
46
|
+
// eslint-disable-next-line no-control-regex
|
|
47
|
+
const CONTROL = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
48
|
+
|
|
49
|
+
/** Text from the server, safe to print: no control characters (no terminal escapes), no token. */
|
|
50
|
+
export function clean(text: unknown, token?: string | null): string {
|
|
51
|
+
let out = String(text ?? "").replace(CONTROL, " ").replace(TOKEN_SHAPE, "[token]");
|
|
52
|
+
if (token) out = out.split(token).join("[token]");
|
|
53
|
+
return out.trim();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const DEVICE_ERRORS = new Set<string>(["authorization_pending", "slow_down", "access_denied", "expired_token"]);
|
|
57
|
+
|
|
58
|
+
export class ApiClient {
|
|
59
|
+
private readonly fetchFn: FetchLike;
|
|
60
|
+
private readonly timeoutMs: number;
|
|
61
|
+
readonly host: string;
|
|
62
|
+
|
|
63
|
+
constructor(private readonly options: ClientOptions) {
|
|
64
|
+
this.fetchFn = options.fetch ?? ((input, init) => fetch(input, init));
|
|
65
|
+
this.timeoutMs = options.timeoutMs ?? 15_000;
|
|
66
|
+
this.host = new URL(options.baseUrl).host;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private async send(method: string, path: string, body?: unknown): Promise<{ status: number; json: unknown; text: string }> {
|
|
70
|
+
const headers: Record<string, string> = { accept: "application/json" };
|
|
71
|
+
if (this.options.token) headers.authorization = `Bearer ${this.options.token}`;
|
|
72
|
+
if (body !== undefined) headers["content-type"] = "application/json";
|
|
73
|
+
let response: Response;
|
|
74
|
+
try {
|
|
75
|
+
response = await this.fetchFn(`${this.options.baseUrl}${path}`, {
|
|
76
|
+
method,
|
|
77
|
+
headers,
|
|
78
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
79
|
+
redirect: "error",
|
|
80
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
81
|
+
});
|
|
82
|
+
} catch (error) {
|
|
83
|
+
const timedOut = error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
|
|
84
|
+
throw new ApiFailure(
|
|
85
|
+
timedOut ? `${this.host} did not answer in ${Math.round(this.timeoutMs / 1000)} seconds.` : `Could not reach ${this.host}.`,
|
|
86
|
+
"check your connection and try again. Free features work without vfrey accounts.",
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
let text = "";
|
|
90
|
+
try {
|
|
91
|
+
text = await response.text();
|
|
92
|
+
} catch {
|
|
93
|
+
text = "";
|
|
94
|
+
}
|
|
95
|
+
let json: unknown = null;
|
|
96
|
+
try {
|
|
97
|
+
json = JSON.parse(text);
|
|
98
|
+
} catch {
|
|
99
|
+
json = null;
|
|
100
|
+
}
|
|
101
|
+
return { status: response.status, json, text };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Maps a non-2xx answer to the CLI's refusal. */
|
|
105
|
+
private fail(status: number, json: unknown, path = ""): ApiFailure {
|
|
106
|
+
const token = this.options.token;
|
|
107
|
+
const err = isApiError(json) ? (json as ApiError).error : null;
|
|
108
|
+
const message = err ? clean(err.message, token) : "";
|
|
109
|
+
if (status === 401) return new ApiFailure("You are not signed in, or this token was revoked.", "run `npx vfrey login`.", 401, "unauthorized");
|
|
110
|
+
if (status === 503 && err?.code === "capacity_paused") {
|
|
111
|
+
return new ApiFailure("vfrey is paused for new data right now.", "try again later. Cached results still work.", 503, "capacity_paused");
|
|
112
|
+
}
|
|
113
|
+
if (status === 402 && err?.code === "credits_required") {
|
|
114
|
+
const url = err.buyCreditsUrl ? clean(err.buyCreditsUrl, token) : "";
|
|
115
|
+
const need = typeof err.required === "number" ? ` It needs ${err.required} credits` : "";
|
|
116
|
+
const have = typeof err.balance === "number" ? `${need ? "; you have" : " You have"} ${err.balance}` : "";
|
|
117
|
+
const detail = need || have ? `${need}${have}.` : "";
|
|
118
|
+
return new ApiFailure(`${message || "Not enough credits."}${detail ? ` ${detail.trim()}` : ""}`, url ? `Buy credits: ${url}` : "buy credits on your vfrey billing page.", 402, "credits_required");
|
|
119
|
+
}
|
|
120
|
+
if (status === 402) {
|
|
121
|
+
const url = err?.upgradeUrl ? clean(err.upgradeUrl, token) : "";
|
|
122
|
+
return new ApiFailure(message || "This feature is not in your plan.", url ? `upgrade your plan at ${url}` : "upgrade your plan at your vfrey billing page.", 402, "plan_required");
|
|
123
|
+
}
|
|
124
|
+
if (status === 429 && err?.code === "rate_limited") {
|
|
125
|
+
return new ApiFailure(message || "Too many requests.", "wait a minute and try again.", 429, "rate_limited");
|
|
126
|
+
}
|
|
127
|
+
if (status === 429) {
|
|
128
|
+
const resets = err?.resetsAt ? clean(err.resetsAt, token) : "";
|
|
129
|
+
const url = err?.upgradeUrl ? clean(err.upgradeUrl, token) : "";
|
|
130
|
+
const fix = resets ? `your limit resets at ${resets}${url ? `, or upgrade at ${url}` : ""}.` : "wait a little and try again.";
|
|
131
|
+
// The server names the meter when it can; otherwise the route says which one ran out.
|
|
132
|
+
const named = (err as { meter?: unknown } | null)?.meter;
|
|
133
|
+
const meter = typeof named === "string" && named ? clean(named, token)
|
|
134
|
+
: /^\/api\/v1\/(topics|niches)/.test(path) ? "topics.requests" : /^\/api\/v1\/creators\/(tiktok|instagram|youtube)\//.test(path) ? "creators.profiles" : "";
|
|
135
|
+
return new ApiFailure(`You reached this month's limit${meter ? ` for ${meter}` : ""}.`, fix, 429, err?.code ?? "limit_reached");
|
|
136
|
+
}
|
|
137
|
+
return new ApiFailure(message || `The vfrey server answered ${status}.`, status >= 500 ? "try again in a moment." : "run `npx vfrey doctor` and check the line above.", status, err?.code);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** A JSON request. Throws ApiFailure for network trouble and any non-2xx answer. */
|
|
141
|
+
async request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
|
142
|
+
const { status, json } = await this.send(method, path, body);
|
|
143
|
+
if (status < 200 || status >= 300) throw this.fail(status, json, path);
|
|
144
|
+
return json as T;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** A request whose answer is plain text (a CSV export). Same refusals as `request`. */
|
|
148
|
+
async requestText(method: string, path: string): Promise<string> {
|
|
149
|
+
const { status, json, text } = await this.send(method, path);
|
|
150
|
+
if (status < 200 || status >= 300) throw this.fail(status, json, path);
|
|
151
|
+
return text;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Creator search (spec 017). Reads the server's index; costs credits. */
|
|
155
|
+
creatorSearch(query: Record<string, string>): Promise<CreatorSearchResponse> {
|
|
156
|
+
return this.request<CreatorSearchResponse>("GET", `/api/v1/creators/search?${new URLSearchParams(query).toString()}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
creatorProfile(platform: string, handle: string): Promise<CreatorProfileView> {
|
|
160
|
+
return this.request<CreatorProfileView>("GET", `/api/v1/creators/${encodeURIComponent(platform)}/${encodeURIComponent(handle)}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
creatorAlerts(): Promise<CreatorAlertsResponse> {
|
|
164
|
+
return this.request<CreatorAlertsResponse>("GET", "/api/v1/creators/alerts");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
watchlists(): Promise<{ watchlists: WatchlistView[] }> {
|
|
168
|
+
return this.request("GET", "/api/v1/watchlists");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
createWatchlist(name: string): Promise<{ id: string; name: string }> {
|
|
172
|
+
return this.request("POST", "/api/v1/watchlists", { name });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
watchlistItems(id: string): Promise<{ watchlist: { id: string; name: string }; items: WatchlistItemView[] }> {
|
|
176
|
+
return this.request("GET", `/api/v1/watchlists/${encodeURIComponent(id)}/items`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
addWatch(id: string, body: { platform: string; handle: string }): Promise<{ ok: boolean; created: boolean }> {
|
|
180
|
+
return this.request("POST", `/api/v1/watchlists/${encodeURIComponent(id)}/items`, body);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
removeWatch(id: string, platform: string, handle: string): Promise<{ ok: boolean }> {
|
|
184
|
+
return this.request("DELETE", `/api/v1/watchlists/${encodeURIComponent(id)}/items?${new URLSearchParams({ platform, handle }).toString()}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
watchlistCsv(id: string): Promise<string> {
|
|
188
|
+
return this.requestText("GET", `/api/v1/watchlists/${encodeURIComponent(id)}/export.csv`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---------- Mail (spec 018). Draft, approve and send are three separate calls; nothing here chains them. ----------
|
|
192
|
+
|
|
193
|
+
mailDraft(body: MailDraftBody): Promise<{ draft: MailDraftView }> {
|
|
194
|
+
return this.request("POST", "/api/v1/mail/drafts", body);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
mailDrafts(query: { status?: string; limit?: number } = {}): Promise<{ drafts: MailDraftView[] }> {
|
|
198
|
+
const params = new URLSearchParams();
|
|
199
|
+
if (query.status) params.set("status", query.status);
|
|
200
|
+
if (query.limit) params.set("limit", String(query.limit));
|
|
201
|
+
const qs = params.toString();
|
|
202
|
+
return this.request("GET", `/api/v1/mail/drafts${qs ? `?${qs}` : ""}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
mailGet(id: string): Promise<{ draft: MailDraftView }> {
|
|
206
|
+
return this.request("GET", `/api/v1/mail/drafts/${encodeURIComponent(id)}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
mailEdit(id: string, patch: MailDraftPatch): Promise<{ draft: MailDraftView }> {
|
|
210
|
+
return this.request("PATCH", `/api/v1/mail/drafts/${encodeURIComponent(id)}`, patch);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
mailApprove(id: string): Promise<{ draft: MailDraftView }> {
|
|
214
|
+
return this.request("POST", `/api/v1/mail/drafts/${encodeURIComponent(id)}/approve`, {});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
mailSend(id: string): Promise<{ draft: MailDraftView }> {
|
|
218
|
+
return this.request("POST", `/api/v1/mail/drafts/${encodeURIComponent(id)}/send`, {});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
mailDiscard(id: string): Promise<{ draft: MailDraftView }> {
|
|
222
|
+
return this.request("POST", `/api/v1/mail/drafts/${encodeURIComponent(id)}/discard`, {});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
mailIdentity(): Promise<{ identity: MailIdentityView | null }> {
|
|
226
|
+
return this.request("GET", "/api/v1/mail/identity");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
mailSaveIdentity(body: MailIdentityBody): Promise<{ identity: MailIdentityView; verificationEmailed: boolean }> {
|
|
230
|
+
return this.request("POST", "/api/v1/mail/identity", body);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Ranked topics for one niche. Costs credits on the server. */
|
|
234
|
+
topics(query: { niche: string; limit?: number; since?: string }): Promise<TopicsResponse> {
|
|
235
|
+
const params = new URLSearchParams({ niche: query.niche });
|
|
236
|
+
if (query.limit !== undefined) params.set("limit", String(query.limit));
|
|
237
|
+
if (query.since) params.set("since", query.since);
|
|
238
|
+
return this.request<TopicsResponse>("GET", `/api/v1/topics?${params.toString()}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
niches(): Promise<NichesResponse> {
|
|
242
|
+
return this.request<NichesResponse>("GET", "/api/v1/niches");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
requestNiche(body: NicheRequestBody): Promise<NicheRequestResponse> {
|
|
246
|
+
return this.request<NicheRequestResponse>("POST", "/api/v1/niches", body);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
me(): Promise<MeResponse> {
|
|
250
|
+
return this.request<MeResponse>("GET", "/api/v1/me");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** The orgs this user belongs to (spec 019 A). */
|
|
254
|
+
orgs(): Promise<OrgsResponse> {
|
|
255
|
+
return this.request<OrgsResponse>("GET", "/api/v1/orgs");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Trades this token for one bound to another org. The server revokes the token used for the call. */
|
|
259
|
+
switchOrg(orgId: string): Promise<OrgSwitchResponse> {
|
|
260
|
+
return this.request<OrgSwitchResponse>("POST", "/api/v1/orgs/switch", { orgId });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Revokes the calling token. */
|
|
264
|
+
async revokeCurrentToken(): Promise<void> {
|
|
265
|
+
const { status, json } = await this.send("DELETE", "/api/v1/tokens/current");
|
|
266
|
+
if (status < 200 || status >= 300) throw this.fail(status, json);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
deviceCode(): Promise<DeviceCodeResponse> {
|
|
270
|
+
return this.request<DeviceCodeResponse>("POST", "/api/v1/device/code", { client_id: CLI_CLIENT_ID });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** One poll. The four device errors come back as data (whatever status carries them); anything else throws. */
|
|
274
|
+
async deviceToken(deviceCode: string): Promise<DeviceTokenResponse> {
|
|
275
|
+
const { status, json } = await this.send("POST", "/api/v1/device/token", { device_code: deviceCode, client_id: CLI_CLIENT_ID });
|
|
276
|
+
const body = json as { error?: unknown; access_token?: unknown } | null;
|
|
277
|
+
if (body && typeof body.error === "string" && DEVICE_ERRORS.has(body.error)) return { error: body.error as DeviceTokenErrorCode };
|
|
278
|
+
if (status >= 200 && status < 300 && body && typeof body.access_token === "string") return json as DeviceTokenResponse;
|
|
279
|
+
if (status >= 200 && status < 300) throw new ApiFailure("The vfrey server sent an answer this version does not understand.", "update vfrey with `npx vfrey update`.", status);
|
|
280
|
+
throw this.fail(status, json);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
// `vfrey login`, `logout`, `whoami`, and the doctor account line.
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import { ApiClient, ApiFailure, clean, type FetchLike } from "./client.js";
|
|
4
|
+
import { apiHost, ApiConfigError, resolveApiUrl } from "./config.js";
|
|
5
|
+
import { deleteToken, loadStoredToken, loadToken, readHint, saveHint, saveToken, type CredentialDeps, type Runner } from "./credentials.js";
|
|
6
|
+
import { formatUserCode, type MeResponse, type OrgSummary } from "./types.js";
|
|
7
|
+
|
|
8
|
+
type Env = Record<string, string | undefined>;
|
|
9
|
+
|
|
10
|
+
export interface ApiIo {
|
|
11
|
+
env: Env;
|
|
12
|
+
stdout: (text: string) => void;
|
|
13
|
+
stderr: (text: string) => void;
|
|
14
|
+
homedir?: string;
|
|
15
|
+
platform?: NodeJS.Platform;
|
|
16
|
+
run?: Runner;
|
|
17
|
+
fetch?: FetchLike;
|
|
18
|
+
sleep?: (ms: number) => Promise<void>;
|
|
19
|
+
now?: () => number;
|
|
20
|
+
/** Opens a URL in the browser. Defaults to the `open` package. */
|
|
21
|
+
openUrl?: (url: string) => Promise<void>;
|
|
22
|
+
timeoutMs?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const NOT_LIVE_FIX = "free features work without an account. If you run your own vfrey server, set VFREY_API_URL or pass --api <url>.";
|
|
26
|
+
|
|
27
|
+
function creds(io: ApiIo): CredentialDeps {
|
|
28
|
+
return { env: io.env, homedir: io.homedir ?? os.homedir(), ...(io.platform ? { platform: io.platform } : {}), ...(io.run ? { run: io.run } : {}) };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function refuse(io: ApiIo, message: string, fix: string): number {
|
|
32
|
+
io.stderr(`✗ ${message}\nFix: ${fix}\n`);
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function failure(io: ApiIo, error: unknown): number {
|
|
37
|
+
if (error instanceof ApiFailure || error instanceof ApiConfigError) return refuse(io, error.message, error.fix);
|
|
38
|
+
return refuse(io, error instanceof Error ? clean(error.message) : "Something went wrong.", "run `npx vfrey doctor` and check the line above.");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function client(io: ApiIo, base: string, token?: string | null): ApiClient {
|
|
42
|
+
return new ApiClient({ baseUrl: base, token: token ?? null, ...(io.fetch ? { fetch: io.fetch } : {}), ...(io.timeoutMs ? { timeoutMs: io.timeoutMs } : {}) });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The flag value after `--api`, or `--api=URL`. */
|
|
46
|
+
function flagValue(args: string[], name: string): string | null | undefined {
|
|
47
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
48
|
+
const arg = args[i]!;
|
|
49
|
+
if (arg === name) return args[i + 1] && !args[i + 1]!.startsWith("--") ? args[i + 1]! : null;
|
|
50
|
+
if (arg.startsWith(`${name}=`)) return arg.slice(name.length + 1);
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Parses the flags these commands share. Returns an error text for an unknown or malformed flag. */
|
|
56
|
+
function parse(args: string[], allowed: string[], valued: string[]): { flags: Set<string>; api?: string; error?: string } {
|
|
57
|
+
const flags = new Set<string>();
|
|
58
|
+
let api: string | undefined;
|
|
59
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
60
|
+
const arg = args[i]!;
|
|
61
|
+
const name = arg.includes("=") ? arg.slice(0, arg.indexOf("=")) : arg;
|
|
62
|
+
if (!allowed.includes(name) && !valued.includes(name)) return { flags, error: `Unknown option "${arg}".` };
|
|
63
|
+
if (valued.includes(name)) {
|
|
64
|
+
const value = flagValue(args, name);
|
|
65
|
+
if (!value) return { flags, error: `${name} needs a value.` };
|
|
66
|
+
api = value;
|
|
67
|
+
if (!arg.includes("=")) i += 1;
|
|
68
|
+
} else flags.add(name);
|
|
69
|
+
}
|
|
70
|
+
return { flags, ...(api ? { api } : {}) };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function resolve(io: ApiIo, api?: string): string | null {
|
|
74
|
+
return resolveApiUrl(io.env, api ?? null);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function notLive(io: ApiIo): number {
|
|
78
|
+
return refuse(io, "vfrey accounts are not live yet.", NOT_LIVE_FIX);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const defaultSleep = (ms: number) => new Promise<void>((done) => setTimeout(done, ms));
|
|
82
|
+
|
|
83
|
+
async function defaultOpen(url: string): Promise<void> {
|
|
84
|
+
const { default: open } = await import("open");
|
|
85
|
+
await open(url);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function browserUrl(url: string): string | null {
|
|
89
|
+
try {
|
|
90
|
+
const parsed = new URL(url);
|
|
91
|
+
return parsed.protocol === "https:" || parsed.protocol === "http:" ? parsed.href : null;
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function runLogin(args: string[], io: ApiIo): Promise<number> {
|
|
98
|
+
const parsed = parse(args, ["--no-browser"], ["--api"]);
|
|
99
|
+
if (parsed.error) return refuse(io, parsed.error, "usage: npx vfrey login [--api URL] [--no-browser].");
|
|
100
|
+
try {
|
|
101
|
+
const base = resolve(io, parsed.api);
|
|
102
|
+
if (!base) return notLive(io);
|
|
103
|
+
const host = apiHost(base);
|
|
104
|
+
const api = client(io, base);
|
|
105
|
+
const sleep = io.sleep ?? defaultSleep;
|
|
106
|
+
const now = io.now ?? Date.now;
|
|
107
|
+
|
|
108
|
+
const code = await api.deviceCode();
|
|
109
|
+
const link = browserUrl(code.verification_uri_complete);
|
|
110
|
+
if (!link) return refuse(io, "The server sent a sign-in link that is not a web address.", "check the API address and try again.");
|
|
111
|
+
io.stdout(`Open ${link} and confirm the code ${formatUserCode(code.user_code)}\n`);
|
|
112
|
+
if (!parsed.flags.has("--no-browser") && !io.env.VFREY_NO_BROWSER) {
|
|
113
|
+
try {
|
|
114
|
+
await (io.openUrl ?? defaultOpen)(link);
|
|
115
|
+
} catch {
|
|
116
|
+
/* the link is printed: the person can open it themselves */
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const deadline = now() + (Number.isFinite(code.expires_in) ? code.expires_in : 600) * 1000;
|
|
121
|
+
let intervalSec = Number.isFinite(code.interval) && code.interval >= 0 ? code.interval : 5;
|
|
122
|
+
let networkErrors = 0;
|
|
123
|
+
for (;;) {
|
|
124
|
+
await sleep(intervalSec * 1000);
|
|
125
|
+
if (now() >= deadline) return refuse(io, "The sign-in code expired.", "run `npx vfrey login` again.");
|
|
126
|
+
let answer;
|
|
127
|
+
try {
|
|
128
|
+
answer = await api.deviceToken(code.device_code);
|
|
129
|
+
networkErrors = 0;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (error instanceof ApiFailure && error.status === undefined && (networkErrors += 1) < 3) continue;
|
|
132
|
+
if (error instanceof ApiFailure && error.status === 429) {
|
|
133
|
+
intervalSec += 5;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
if ("access_token" in answer) {
|
|
139
|
+
const org = clean(answer.org.name);
|
|
140
|
+
let plan = "";
|
|
141
|
+
try {
|
|
142
|
+
plan = clean((await client(io, base, answer.access_token).me()).plan);
|
|
143
|
+
} catch {
|
|
144
|
+
/* signed in all the same; the plan shows on whoami */
|
|
145
|
+
}
|
|
146
|
+
let saved;
|
|
147
|
+
try {
|
|
148
|
+
saved = saveToken(host, answer.access_token, { org, plan }, creds(io));
|
|
149
|
+
} catch {
|
|
150
|
+
return refuse(io, "Signed in, but the token could not be saved on this machine.", "make the vfrey home folder writable (VFREY_HOME), then run `npx vfrey login` again.");
|
|
151
|
+
}
|
|
152
|
+
io.stdout(`✓ Signed in to ${org}${plan ? ` (${plan})` : ""}\n`);
|
|
153
|
+
if (saved.where === "file") io.stdout(`Token saved in ${saved.file} (readable only by you).\n`);
|
|
154
|
+
if (io.env.VFREY_TOKEN) io.stderr("Note: VFREY_TOKEN is set in this shell and overrides the token you just saved.\n");
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
if (answer.error === "slow_down") intervalSec += 5;
|
|
158
|
+
else if (answer.error === "access_denied") return refuse(io, "Sign-in was denied in the browser.", "run `npx vfrey login` again and choose Approve.");
|
|
159
|
+
else if (answer.error === "expired_token") return refuse(io, "The sign-in code expired.", "run `npx vfrey login` again.");
|
|
160
|
+
}
|
|
161
|
+
} catch (error) {
|
|
162
|
+
return failure(io, error);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function runLogout(args: string[], io: ApiIo): Promise<number> {
|
|
167
|
+
const parsed = parse(args, [], ["--api"]);
|
|
168
|
+
if (parsed.error) return refuse(io, parsed.error, "usage: npx vfrey logout [--api URL].");
|
|
169
|
+
try {
|
|
170
|
+
const base = resolve(io, parsed.api);
|
|
171
|
+
if (!base) {
|
|
172
|
+
io.stdout("✓ Not signed in (vfrey accounts are not live yet).\n");
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
const host = apiHost(base);
|
|
176
|
+
const stored = loadStoredToken(host, creds(io));
|
|
177
|
+
if (!stored) {
|
|
178
|
+
io.stdout(`✓ Not signed in to ${host}.\n`);
|
|
179
|
+
if (io.env.VFREY_TOKEN) io.stderr("Note: VFREY_TOKEN is set in this shell; unset it to stop using that token.\n");
|
|
180
|
+
return 0;
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
await client(io, base, stored.token).revokeCurrentToken();
|
|
184
|
+
} catch (error) {
|
|
185
|
+
// 401 means the server already treats the token as revoked. Anything else leaves it live: say so, still sign out here.
|
|
186
|
+
if (!(error instanceof ApiFailure && error.status === 401)) {
|
|
187
|
+
io.stderr(`! Could not revoke the token on ${host} (${error instanceof ApiFailure ? error.message : "unknown error"}). It stays valid there until you revoke it at ${base}/settings/tokens.\n`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
deleteToken(host, creds(io));
|
|
191
|
+
io.stdout(`✓ Signed out of ${host}\n`);
|
|
192
|
+
if (io.env.VFREY_TOKEN) io.stderr("Note: VFREY_TOKEN is set in this shell; unset it to stop using that token.\n");
|
|
193
|
+
return 0;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
return failure(io, error);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function formatMe(me: MeResponse): string {
|
|
200
|
+
const lines = [
|
|
201
|
+
`✓ Signed in as ${clean(me.user.email)}`,
|
|
202
|
+
`Org: ${clean(me.org.name)} (${clean(me.org.role)})`,
|
|
203
|
+
`Plan: ${clean(me.plan)}`,
|
|
204
|
+
`Entitlements: ${Object.entries(me.entitlements).map(([name, on]) => `${clean(name)} ${on ? "yes" : "no"}`).join(", ")}`,
|
|
205
|
+
];
|
|
206
|
+
if (me.credits) {
|
|
207
|
+
const soon = me.credits.expiringSoon > 0 ? ` (${me.credits.expiringSoon} expiring soon)` : "";
|
|
208
|
+
lines.push(`Credits: ${me.credits.balance}${soon}`);
|
|
209
|
+
}
|
|
210
|
+
const meters = Object.entries(me.usage ?? {});
|
|
211
|
+
if (meters.length > 0) {
|
|
212
|
+
lines.push("Usage this month:");
|
|
213
|
+
for (const [meter, reading] of meters) {
|
|
214
|
+
lines.push(` ${clean(meter)}: ${reading.used} of ${reading.limit ?? "unlimited"}, resets ${clean(reading.resetsAt)}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (me.upgradeUrl) lines.push(`Upgrade or manage: ${clean(me.upgradeUrl)}`);
|
|
218
|
+
return lines.join("\n") + "\n";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function runWhoami(args: string[], io: ApiIo): Promise<number> {
|
|
222
|
+
const parsed = parse(args, ["--json"], ["--api"]);
|
|
223
|
+
if (parsed.error) return refuse(io, parsed.error, "usage: npx vfrey whoami [--json] [--api URL].");
|
|
224
|
+
try {
|
|
225
|
+
const base = resolve(io, parsed.api);
|
|
226
|
+
if (!base) return notLive(io);
|
|
227
|
+
const host = apiHost(base);
|
|
228
|
+
const found = loadToken(host, creds(io));
|
|
229
|
+
if (!found) return refuse(io, "You are not signed in.", "run `npx vfrey login`.");
|
|
230
|
+
const me = await client(io, base, found.token).me();
|
|
231
|
+
if (found.source !== "env") saveHint(host, { org: clean(me.org.name), plan: clean(me.plan) }, creds(io));
|
|
232
|
+
io.stdout(parsed.flags.has("--json") ? JSON.stringify(me, null, 2) + "\n" : formatMe(me));
|
|
233
|
+
return 0;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
return failure(io, error);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const ORG_USAGE = "usage: npx vfrey org list [--json] [--api URL] | npx vfrey org use ORG [--api URL].";
|
|
240
|
+
|
|
241
|
+
/** The org named by an id or an exact name (any case). Null when none match; "many" when a name is not unique. */
|
|
242
|
+
function pickOrg(orgs: OrgSummary[], wanted: string): OrgSummary | null | "many" {
|
|
243
|
+
const byId = orgs.find((o) => o.id === wanted);
|
|
244
|
+
if (byId) return byId;
|
|
245
|
+
const byName = orgs.filter((o) => o.name.toLowerCase() === wanted.toLowerCase());
|
|
246
|
+
return byName.length === 1 ? byName[0]! : byName.length > 1 ? "many" : null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* `vfrey org list` and `vfrey org use ORG` (spec 019 A). A token is bound to one org for life, so `use` asks the server
|
|
251
|
+
* to trade the stored token for one bound to the chosen org (the server checks membership and revokes the old one),
|
|
252
|
+
* then stores the new token. A token from VFREY_TOKEN is never replaced: it stays what the shell says it is.
|
|
253
|
+
*/
|
|
254
|
+
export async function runOrg(args: string[], io: ApiIo): Promise<number> {
|
|
255
|
+
const [sub, ...rest] = args;
|
|
256
|
+
if (sub !== "list" && sub !== "use") return refuse(io, sub ? `Unknown org command "${clean(sub)}".` : "Say what to do with orgs.", ORG_USAGE);
|
|
257
|
+
const positional: string[] = [];
|
|
258
|
+
const flagArgs: string[] = [];
|
|
259
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
260
|
+
const arg = rest[i]!;
|
|
261
|
+
if (arg.startsWith("--")) {
|
|
262
|
+
flagArgs.push(arg);
|
|
263
|
+
if (arg === "--api" && rest[i + 1] !== undefined) flagArgs.push(rest[(i += 1)]!);
|
|
264
|
+
} else positional.push(arg);
|
|
265
|
+
}
|
|
266
|
+
const parsed = parse(flagArgs, sub === "list" ? ["--json"] : [], ["--api"]);
|
|
267
|
+
if (parsed.error) return refuse(io, parsed.error, ORG_USAGE);
|
|
268
|
+
if (positional.length > (sub === "use" ? 1 : 0)) return refuse(io, `Unexpected "${clean(positional[sub === "use" ? 1 : 0])}".`, ORG_USAGE);
|
|
269
|
+
const wanted = sub === "use" ? positional[0] : undefined;
|
|
270
|
+
if (sub === "use" && !wanted) return refuse(io, "Say which org to use.", "run `npx vfrey org list`, then `npx vfrey org use ORG` with its name or id.");
|
|
271
|
+
try {
|
|
272
|
+
const base = resolve(io, parsed.api);
|
|
273
|
+
if (!base) return notLive(io);
|
|
274
|
+
const host = apiHost(base);
|
|
275
|
+
const found = loadToken(host, creds(io));
|
|
276
|
+
if (!found) return refuse(io, "You are not signed in.", "run `npx vfrey login`.");
|
|
277
|
+
const api = client(io, base, found.token);
|
|
278
|
+
const { orgs } = await api.orgs();
|
|
279
|
+
if (sub === "list") {
|
|
280
|
+
if (parsed.flags.has("--json")) io.stdout(JSON.stringify({ orgs }, null, 2) + "\n");
|
|
281
|
+
else io.stdout(orgs.map((o) => `${o.current ? "*" : " "} ${clean(o.name)} (${clean(o.role)}) ${clean(o.id)}`).join("\n") + "\n");
|
|
282
|
+
return 0;
|
|
283
|
+
}
|
|
284
|
+
const target = pickOrg(orgs, wanted!);
|
|
285
|
+
if (target === "many") return refuse(io, `More than one org is named "${clean(wanted)}".`, "use the id from `npx vfrey org list`.");
|
|
286
|
+
if (!target) return refuse(io, `You are not in an org called "${clean(wanted)}".`, "run `npx vfrey org list` to see yours.");
|
|
287
|
+
if (target.current) {
|
|
288
|
+
io.stdout(`✓ Already using ${clean(target.name)} (${clean(target.role)})\n`);
|
|
289
|
+
return 0;
|
|
290
|
+
}
|
|
291
|
+
if (found.source === "env") return refuse(io, "VFREY_TOKEN is set, and that token is bound to one org.", "unset VFREY_TOKEN, run `npx vfrey login`, then `npx vfrey org use` again.");
|
|
292
|
+
let switched;
|
|
293
|
+
try {
|
|
294
|
+
switched = await api.switchOrg(target.id);
|
|
295
|
+
} catch (error) {
|
|
296
|
+
if (error instanceof ApiFailure && error.status === 403) return refuse(io, "This token is fixed to its org.", `run \`npx vfrey login\` while ${clean(target.name)} is selected in the vfrey console.`);
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
let plan = "";
|
|
300
|
+
try {
|
|
301
|
+
plan = clean((await client(io, base, switched.access_token).me()).plan);
|
|
302
|
+
} catch {
|
|
303
|
+
/* switched all the same; the plan shows on whoami */
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
saveToken(host, switched.access_token, { org: clean(switched.org.name), plan }, creds(io));
|
|
307
|
+
} catch {
|
|
308
|
+
return refuse(io, "Switched, but the new token could not be saved on this machine.", "make the vfrey home folder writable (VFREY_HOME), then run `npx vfrey login`.");
|
|
309
|
+
}
|
|
310
|
+
io.stdout(`✓ Now using ${clean(switched.org.name)} (${clean(switched.org.role)})\n`);
|
|
311
|
+
return 0;
|
|
312
|
+
} catch (error) {
|
|
313
|
+
return failure(io, error);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** The doctor line. Local only (no network, no Keychain), and never a failure. */
|
|
318
|
+
export function accountLine(env: Env, homedir: string = os.homedir()): string {
|
|
319
|
+
const none = "not signed in (free features work without it)";
|
|
320
|
+
try {
|
|
321
|
+
const base = resolveApiUrl(env, null);
|
|
322
|
+
if (!base) return none;
|
|
323
|
+
const host = apiHost(base);
|
|
324
|
+
const hint = readHint(host, { env, homedir });
|
|
325
|
+
if (env.VFREY_TOKEN?.trim()) return hint ? `signed in as ${hint.org} (${hint.plan}) with VFREY_TOKEN` : "signed in with VFREY_TOKEN";
|
|
326
|
+
return hint ? `signed in as ${hint.org} (${hint.plan})` : none;
|
|
327
|
+
} catch {
|
|
328
|
+
return none;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Where the vfrey accounts server is. Set DEFAULT_API_URL at release, when the console is deployed.
|
|
2
|
+
|
|
3
|
+
type Env = Record<string, string | undefined>;
|
|
4
|
+
|
|
5
|
+
/** The release-time default. Unset until the server is live: `login` then refuses without any network call. */
|
|
6
|
+
export const DEFAULT_API_URL: string | null = null;
|
|
7
|
+
|
|
8
|
+
export class ApiConfigError extends Error {
|
|
9
|
+
constructor(message: string, readonly fix: string) {
|
|
10
|
+
super(message);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** `--api URL`, else VFREY_API_URL, else the release default. Null means accounts are not live. */
|
|
15
|
+
export function resolveApiUrl(env: Env, flag?: string | null): string | null {
|
|
16
|
+
const raw = (flag ?? env.VFREY_API_URL ?? DEFAULT_API_URL ?? "").trim();
|
|
17
|
+
if (!raw) return null;
|
|
18
|
+
let url: URL;
|
|
19
|
+
try {
|
|
20
|
+
url = new URL(raw);
|
|
21
|
+
} catch {
|
|
22
|
+
throw new ApiConfigError(`"${raw}" is not a URL.`, "pass a full address such as https://vfrey.example.com with --api or VFREY_API_URL.");
|
|
23
|
+
}
|
|
24
|
+
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
|
|
25
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
|
|
26
|
+
throw new ApiConfigError(`${url.origin} is not https. vfrey sends your token only over https (or to this machine).`, "use an https:// address.");
|
|
27
|
+
}
|
|
28
|
+
return url.origin;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The key a token is stored under: the API's host (with port when it has one). */
|
|
32
|
+
export function apiHost(base: string): string {
|
|
33
|
+
return new URL(base).host;
|
|
34
|
+
}
|