@homie-rocks/studio 0.24.0 → 0.24.3
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/CHANGELOG.md +53 -1
- package/README.md +34 -3
- package/bin/homie-studio.mjs +44 -6
- package/lib/cloudflare.mjs +11 -3
- package/lib/doctor.mjs +50 -5
- package/lib/projects-env.mjs +68 -0
- package/lib/projects.mjs +311 -0
- package/lib/scaffold.mjs +13 -2
- package/lib/shop-catalog.mjs +163 -0
- package/lib/shop.mjs +151 -60
- package/lib/template-history.json +78 -0
- package/package.json +1 -1
- package/shop/SELLING.md +16 -0
- package/shop/SHOP.md +65 -16
- package/worker/office.mjs +2 -1
- package/worker/shop-rules.mjs +17 -1
- package/worker/shop.mjs +32 -9
- package/worker/stripe.mjs +30 -0
- package/worker/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -17,9 +17,61 @@ To bring a studio up to date, tell Claude: "Upgrade my studio to the newest Homi
|
|
|
17
17
|
`npx -y @homie-rocks/studio@latest upgrade`, which shows what's new since the version your studio pins (from this
|
|
18
18
|
file) and what the upgrade would change, and changes nothing until you agree.
|
|
19
19
|
|
|
20
|
+
## [0.24.3] - 2026-10-03
|
|
21
|
+
|
|
22
|
+
**Plugin 0.25.3** · [#36](https://github.com/homie-rocks/homie/pull/36)
|
|
23
|
+
|
|
24
|
+
Set up your shop with Stripe's own tools for AI: your AI makes the products, checks tax, makes the webhook and answers "how are sales?"; you make the account, approve Stripe's pages and paste one key.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- Stripe's agent plugin is how your AI works with your Stripe (`npm install -g @stripe/cli@latest && stripe agent setup`: Stripe's own MCP server and skills for Claude Code and Codex, or Stripe's connector in the Claude app). It is set up when your studio starts selling, never before: `setup status` shows a Stripe row once `shop.json` exists. You sign in once on Stripe's page and give it a sandbox first.
|
|
29
|
+
- `homie-studio shop catalog`: your items as Products in Stripe, made by your AI through Stripe's MCP. It names the one read to make, then (`--have <Stripe's answer>`) exactly the writes still needed: a Product per item with a tax code eligible for Managed Payments and a default Price of `shop.json`'s amount. A new price becomes a new default Price; an item you removed is archived, never deleted; products you made yourself are never touched. In sync, `shop.json` records `"catalog": ["test"]` and checkouts name each item's Product, so Stripe's Dashboard and reports see sales by product. `shop.json`'s price is still what is charged.
|
|
30
|
+
- `homie-studio shop connect` now makes the webhook itself: you paste one restricted key (with Webhook Endpoints: Write added to its permissions), the page makes the endpoint with it, and Stripe's signing secret goes straight to your Worker without anyone seeing it. An older endpoint the kit made for the same address is turned off, not deleted. A key without that permission still works with a webhook secret you made yourself.
|
|
31
|
+
- With Managed Payments chosen in test mode, the connect page tries one test checkout with it (expired at once) and tells you whether Stripe has it on for your account.
|
|
32
|
+
- A refund Stripe holds for approval (when the shop's key is an Agent key) is shown as held, not failed: approve it in Stripe and the item leaves the player's account when Stripe refunds. Refunds made in Stripe's Dashboard or through Stripe's MCP take the item back the same way.
|
|
33
|
+
- `homie-studio setup --via stripe-projects [--with elevenlabs]` (a prototype): for a creator with no Cloudflare account, Stripe Projects makes or links one (and ElevenLabs) on their own Stripe sign-in, on free plans. It stops at each step that is yours (Stripe's sign-in, the providers' terms, Cloudflare's approval page), puts the account id in `studio.json`, keeps the token in Projects' vault and its git-ignored `.env`, and every Wrangler run in the studio uses it. `npx wrangler login` stays the default.
|
|
34
|
+
- The plugin's shop skill now walks a creator with a fresh Stripe account through it: what you click, what to tell your AI, and what stays yours. The studio-setup skill has the Stripe Projects option.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- The Homie mod refuses a write through Stripe's MCP that would make a webhook endpoint (or an event destination with its secret): Stripe answers the signing secret in that call, and it would land in the conversation. The connect page makes the webhook instead.
|
|
39
|
+
- The Homie mod now takes Stripe webhook signing secrets (`whsec_…`) out of tool output, as it does Stripe keys.
|
|
40
|
+
- `providers.json` lists Stripe: its agent plugin and MCP server for the shop skill, and Stripe Projects for studio-setup. The shop skill declares Stripe's MCP server for Codex. The Homie mod holds `stripe projects upgrade`, `billing add` / `billing update` and anything with `--confirm-paid-service` as paid calls whose cost cannot be read first.
|
|
41
|
+
- A new studio's `.gitignore` also keeps `.env.*`, `.projects/vault/` and `.projects/cache/` out of git; `upgrade` adds them to yours.
|
|
42
|
+
|
|
43
|
+
### Upgrade notes
|
|
44
|
+
|
|
45
|
+
- Stripe's MCP accepts only its sign-in page (OAuth) or keys tagged "Agent" from 2026-10-31. If you gave an AI a plain Stripe key for the MCP, replace it with an Agent key or sign in again. The shop's own key in your Worker is not affected, and must stay a plain restricted key.
|
|
46
|
+
- To have the connect page make your webhook, add Webhook Endpoints: Write to the shop's restricted key (or make a new key) and run `homie-studio shop connect` again. Afterwards you can set that permission back to None.
|
|
47
|
+
|
|
48
|
+
## [0.24.2] - 2026-10-03
|
|
49
|
+
|
|
50
|
+
**Plugin 0.25.2** · [#35](https://github.com/homie-rocks/homie/pull/35) · [release-2026-10-03-studio-0.24.2](https://github.com/homie-rocks/homie/releases/tag/release-2026-10-03-studio-0.24.2)
|
|
51
|
+
|
|
52
|
+
Homie's skills work through each provider's own CLI, plugin and MCP server, declared so they load only when a skill needs them, and the Homie mod holds the paid calls and Cloudflare changes those tools can make.
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
|
|
56
|
+
- `providers.json` in the plugin: each provider's own CLI, plugin, MCP servers and skills, the skills that use them, and what stays Homie's (budgets and receipts, the kids rules, secrets never in the chat, the owner's one-tap asks, phone budgets, the rights notes). The plugin's `plugin.json` points at it (`extensions["rocks.homie"].providers`). Nothing installs by itself: a skill offers a provider's tool when the person wants what it unlocks, and the person approves the install and signs in on the provider's own page.
|
|
57
|
+
- Each skill that uses a provider names it in its own frontmatter (`metadata.providers`, and `compatibility` in words: the Agent Skills fields). The art, video, models, style, publish and servers skills declare their provider's MCP server for Codex in `agents/openai.yaml` (fal's MCP server; Cloudflare's docs server, which needs no sign-in), so Codex can wire it when the skill is used.
|
|
58
|
+
- The Homie mod holds a change to a studio's Cloudflare account made outside its deploy, inside the studio: Wrangler deleting something (a Worker, a D1 database, an R2 bucket or object, a KV namespace or key, a queue, a secret), a `secret put`, a version rolled out or back by hand, a migration or a writing query on the live database (`--remote`), and the same through Cloudflare's own MCP servers or a claude.ai Cloudflare connector (the API server's `execute` sending anything but a GET or a GraphQL read; a tool that deletes, updates or edits). The hold names the studio's own Worker, database or bucket when the change touches one. Creating something and reading are not held; `guardDeploys: false` turns it off.
|
|
59
|
+
- The mod's spending guard knows the providers' own CLIs and MCP servers: a generating command of `elevenlabs`, `fal api` or `fal run`, `genmedia run` and `tripo` is held as a call whose cost cannot be read first, and so is a run on fal's, ElevenLabs' or Tripo's MCP server. Their help, dry runs, sign-ins, prices, listings and ElevenLabs' `estimate_only` are free.
|
|
60
|
+
|
|
61
|
+
### Changed
|
|
62
|
+
|
|
63
|
+
- The art and video skills find models and read prices on fal's own MCP server at its sign-in address (`https://mcp.fal.ai/mcp-relay`: the person signs in on fal's page, no key), with the commands that connect it in Claude Code and in Codex. Paid runs still go through the skills' scripts (priced, capped, receipted, resumable), never the MCP's `run_model`.
|
|
64
|
+
- The publish skill points at Cloudflare's own tools for questions beyond the deploy (its docs MCP server; its plugin's skills and API server, from `cloudflare/skills` or Claude's plugin directory; `wrangler login --device` where no browser can open) and says the studio's own Worker, database, storage and secrets change only through its deploy. The servers skill reads Cloudflare's model catalogue (`wrangler ai models list --json`) rather than memory.
|
|
65
|
+
- The music skill names ElevenLabs' plugin for Codex too, their skills on their own (`npx skills add elevenlabs/skills`) and their hosted MCP server, which has no music tools, and prefers the CLI to a raw API call.
|
|
66
|
+
- The models skill keeps Tripo on fal: Tripo's own CLI and MCP server bill a separate Tripo account outside the skill's budget and receipts.
|
|
67
|
+
|
|
68
|
+
### Fixed
|
|
69
|
+
|
|
70
|
+
- The studio-setup and style skills' descriptions are plain YAML now. Each held a `: `, which a strict YAML reader refuses.
|
|
71
|
+
|
|
20
72
|
## [0.24.0] - 2026-10-03
|
|
21
73
|
|
|
22
|
-
**Plugin 0.25.0** · [#34](https://github.com/homie-rocks/homie/pull/34)
|
|
74
|
+
**Plugin 0.25.0** · [#34](https://github.com/homie-rocks/homie/pull/34) · [release-2026-10-03-studio-0.24.0](https://github.com/homie-rocks/homie/releases/tag/release-2026-10-03-studio-0.24.0)
|
|
23
75
|
|
|
24
76
|
Sell things in your games with your own Stripe account: a supporter pack, cosmetics, a pass, an unlock or a tip, in real money, with refunds from your office and the kids rules built in; Homie never sees the money.
|
|
25
77
|
|
package/README.md
CHANGED
|
@@ -325,7 +325,7 @@ npx homie-studio chat words # the bu
|
|
|
325
325
|
|
|
326
326
|
## Selling things: the shop
|
|
327
327
|
|
|
328
|
-
A studio sells items for its games (0.24.0) with **its own Stripe account**: the studio is the seller, money goes
|
|
328
|
+
A studio sells items for its games (0.24.0; Stripe's own agent tools since 0.24.3) with **its own Stripe account**: the studio is the seller, money goes
|
|
329
329
|
straight from players to its Stripe, and homie.rocks never sees, holds or moves it (Homie takes no cut; no shared
|
|
330
330
|
currency; not a Connect platform). Real-money prices only, nothing random, the kids rules built in (no shop on a kids
|
|
331
331
|
server, spending off until a neutral age question says adult, nothing for under-13s, 13-17 through a parent's own
|
|
@@ -334,11 +334,20 @@ checkout, a monthly cap), and the television only shows a code to buy on a phone
|
|
|
334
334
|
```sh
|
|
335
335
|
npx homie-studio shop init --supporter # shop.json with a US$5 Supporter pack, and SELLING.md (the owner's plain words)
|
|
336
336
|
npx homie-studio shop check # the kit's rules; every build checks them too
|
|
337
|
-
npx homie-studio shop
|
|
337
|
+
npx homie-studio shop catalog [--have <file>] # the items as Products in Stripe: the read, then the exact writes (Stripe's MCP)
|
|
338
|
+
npx homie-studio shop connect # a page on THIS computer: the owner pastes one restricted key; it makes the webhook
|
|
338
339
|
npx homie-studio shop # open (test or live) or what is missing
|
|
339
340
|
npx homie-studio shop refund ord_… # an ASK: the owner taps once (or Refund in /_studio/office/shop)
|
|
340
341
|
```
|
|
341
342
|
|
|
343
|
+
Since 0.24.3 the owner's AI sets it up with **Stripe's own agent tools** (`npm install -g @stripe/cli@latest && stripe
|
|
344
|
+
agent setup`: Stripe's MCP server and skills; the owner signs in once on Stripe's page, a sandbox first): it makes the
|
|
345
|
+
catalog through the MCP (`shop catalog` says exactly what to write), reads the tax settings, and answers "how are
|
|
346
|
+
sales?" read-only. The owner makes the account, approves Stripe's pages and pastes **one** restricted key on the
|
|
347
|
+
connect page, which makes the webhook with it, so its signing secret goes straight to the Worker and never through a
|
|
348
|
+
chat. A webhook is never made through Stripe's MCP (its answer carries the secret). From 2026-10-31 Stripe's MCP takes
|
|
349
|
+
only OAuth or Agent-tagged keys; the shop's own key is a plain restricted key and is not affected.
|
|
350
|
+
|
|
342
351
|
In a game: `createShop()` from `@homie-rocks/studio/shop` (`has`, `entitlements`, `on('change')`, `open`, `used`); a
|
|
343
352
|
supporter's badge rides on their seat (`peer.badge`). Stripe Checkout (hosted), Stripe Tax on, or Stripe Managed
|
|
344
353
|
Payments (Stripe as seller of record, 3.5% more) as one switch; a signed, idempotent webhook; refunds from the
|
|
@@ -356,7 +365,8 @@ One checklist of what this computer and the person's accounts have for a studio:
|
|
|
356
365
|
connector (the AI says whether its Homie tools are there; the directory is reached), Cloudflare (signed in
|
|
357
366
|
with the studio's own Wrangler, and the account's email verified: a deploy that went through proves it, a
|
|
358
367
|
deploy refused for it is remembered), Chrome for the checks, ffmpeg, and the optional GitHub, ElevenLabs and
|
|
359
|
-
fal (and, in Claude Code, the status line)
|
|
368
|
+
fal (and, in Claude Code, the status line); in a studio that sells (shop.json), a Stripe row: whether Stripe's own
|
|
369
|
+
agent plugin (its MCP server) is set up for this AI, with `stripe agent setup` as the fix (0.24.3). Each row is green, missing or "do this now", says what it
|
|
360
370
|
unlocks, and gives the exact fix: a command the AI runs or a page the person taps. It only reads, answers in
|
|
361
371
|
seconds, and never prints a key, a token or an account's name or id. A studio the Claude app's setup card made
|
|
362
372
|
(`setup attach`) counts as connected to Cloudflare and GitHub there. `--json` gives the rows.
|
|
@@ -613,6 +623,27 @@ the home folder's `.cache/homie-studio` (from storage.googleapis.com, which a cl
|
|
|
613
623
|
renders with SwiftShader. `check` reports how fast each browser drew the game and on what renderer; on SwiftShader
|
|
614
624
|
it says the frame rate is not a person's, and judges only seats, rooms and rounds.
|
|
615
625
|
|
|
626
|
+
## Another way to Cloudflare: Stripe Projects (a prototype, 0.24.3)
|
|
627
|
+
|
|
628
|
+
`npx wrangler login` stays the way. For a creator with no Cloudflare account (and no ElevenLabs), Stripe Projects
|
|
629
|
+
(Stripe's CLI plugin, docs.stripe.com/projects) can make or link both on the person's own Stripe sign-in, on free
|
|
630
|
+
plans, and hand the credentials to the studio folder:
|
|
631
|
+
|
|
632
|
+
```sh
|
|
633
|
+
npx homie-studio setup --via stripe-projects [--with elevenlabs] --dry-run # what it would do; changes nothing
|
|
634
|
+
npx homie-studio setup --via stripe-projects [--with elevenlabs] --accept-tos # after the person accepted the providers' terms
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
It stops with a `needs` step at each thing that is the person's (the Stripe CLI and its Projects plugin, Stripe's
|
|
638
|
+
sign-in, the providers' terms, Cloudflare's own approval page for an existing account); it never adds a paid plan or
|
|
639
|
+
a payment method. The Cloudflare account id goes in studio.json with `"auth": "stripe-projects"`; the token stays in
|
|
640
|
+
Projects' vault and the git-ignored `.env` it syncs, and every Wrangler run in the studio takes it from there
|
|
641
|
+
(`deploy`, `dev`, secrets, the office). It checks read-only what the token can do (Wrangler signed in, D1, Workers AI;
|
|
642
|
+
R2 is optional) and that `.gitignore` keeps `.env`, `.env.*`, `.projects/vault/` and `.projects/cache/` out of git.
|
|
643
|
+
With `--with elevenlabs` the music skill finds the ElevenLabs key there too (ElevenLabs' free plan has no commercial
|
|
644
|
+
licence). Unverified until a real run: the exact JSON of the Projects commands, and the Cloudflare token's permission
|
|
645
|
+
groups, which the checks measure instead of assuming.
|
|
646
|
+
|
|
616
647
|
## No payment method needed
|
|
617
648
|
|
|
618
649
|
`deploy` creates only what Cloudflare's free Workers plan gives a new account with no
|
package/bin/homie-studio.mjs
CHANGED
|
@@ -125,8 +125,12 @@
|
|
|
125
125
|
* homie-studio shop init [--supporter] [--currency usd] [--price 500] [--managed] shop.json and SELLING.md
|
|
126
126
|
* homie-studio shop check shop.json against the kit's rules (real money, nothing random, the kids rules)
|
|
127
127
|
* homie-studio shop connect [--managed] [--live] a page on THIS computer where the owner pastes the studio's
|
|
128
|
-
* restricted Stripe key
|
|
129
|
-
*
|
|
128
|
+
* restricted Stripe key; with it this makes the webhook (0.24.3), and the key
|
|
129
|
+
* and the webhook's secret go straight to the Worker secrets, never a chat or a
|
|
130
|
+
* file (test keys only unless --live)
|
|
131
|
+
* homie-studio shop catalog [--have <file>|-] [--mode test|live] the items as Products in the studio's Stripe,
|
|
132
|
+
* made by the AI through Stripe's own MCP: the read first, then (with what it
|
|
133
|
+
* answered) the exact writes; in sync, shop.json "catalog" records the mode
|
|
130
134
|
* homie-studio shop disconnect both secrets gone: the shop closes
|
|
131
135
|
* homie-studio shop orders the latest orders (never a card or an email)
|
|
132
136
|
* homie-studio shop refund <order> [--reason …] [--note "<why>"] ASKS the owner (a one-tap link)
|
|
@@ -162,6 +166,13 @@
|
|
|
162
166
|
* the build so the chat's card follows it, and print the steps; HANDOFF.md)
|
|
163
167
|
* homie-studio setup attach <hs_…> (this repository is the studio the Claude app's setup card is making: say so, once,
|
|
164
168
|
* learn its live address, and give a template copy its real name)
|
|
169
|
+
* homie-studio setup --via stripe-projects [--with elevenlabs] [--accept-tos] [--dry-run]
|
|
170
|
+
* (a prototype, 0.24.3: Cloudflare, and ElevenLabs with --with, made or linked
|
|
171
|
+
* through Stripe Projects on their free plans; the person accepts the providers'
|
|
172
|
+
* terms (then --accept-tos) and signs in on Stripe's and the provider's own
|
|
173
|
+
* pages; the account id goes into studio.json, the token stays in Projects'
|
|
174
|
+
* vault and its git-ignored .env, and Wrangler runs with it here. The usual
|
|
175
|
+
* way, npx wrangler login, stays the default. lib/projects.mjs)
|
|
165
176
|
* homie-studio setup status [--connector yes|no] (also: homie-studio doctor)
|
|
166
177
|
* what this computer and the person's accounts have for a studio: Node, the Homie
|
|
167
178
|
* connector, Cloudflare (signed in, email verified), Chrome, ffmpeg, GitHub,
|
|
@@ -237,6 +248,9 @@ import { chatBudget, chatLines, chatRemove, chatRulesSet, chatShow, chatWords }
|
|
|
237
248
|
import { agentsBrain, agentsBrainKey, agentsPass, agentsPasses, agentsRevoke, serversClose, serversLevel, serversLines, serversList, serversMember, serversNew, serversSet } from '../lib/servers.mjs';
|
|
238
249
|
import { AgentSeat } from '../lib/agent-seat.mjs';
|
|
239
250
|
import { shopCheck, shopConnect, shopDisconnect, shopInit, shopLines, shopOrders, shopRefund, shopStatements, shopStatus } from '../lib/shop.mjs';
|
|
251
|
+
import { catalogLines, catalogPlan } from '../lib/shop-catalog.mjs';
|
|
252
|
+
import { projectsLines, setupViaProjects } from '../lib/projects.mjs';
|
|
253
|
+
import { projectsCloudflareEnv } from '../lib/projects-env.mjs';
|
|
240
254
|
import { Feed, currentFeed, currentId, flushProgress, publicFeed, readFeed, recordChange, startProgress } from '../lib/progress.mjs';
|
|
241
255
|
import { formatStatus, setupStatus } from '../lib/doctor.mjs';
|
|
242
256
|
import { codexTarget, newCodex, writeCodexPage } from '../lib/codex.mjs';
|
|
@@ -249,7 +263,7 @@ import { formatHandoff, handoff } from '../lib/handoff.mjs';
|
|
|
249
263
|
|
|
250
264
|
const argv = process.argv.slice(2);
|
|
251
265
|
const flags = new Map();
|
|
252
|
-
const BOOL_FLAGS = ['off', 'revoke', 'json', 'yes', 'detach', 'no-install', 'plan', 'stop', 'share', 'apply', 'diff', 'template', 'ci', 'fresh', 'install', 'remove', 'replace', 'artifact', 'open', 'reopen', 'kids', 'remote-ai', 'dry-run', 'verify', 'maps', 'profile', 'hands-on', 'automatic', 'unlock', 'confirm', 'no-library', 'no-validate', 'rigged', 'supporter', 'managed', 'live', 'send'];
|
|
266
|
+
const BOOL_FLAGS = ['off', 'revoke', 'json', 'yes', 'detach', 'no-install', 'plan', 'stop', 'share', 'apply', 'diff', 'template', 'ci', 'fresh', 'install', 'remove', 'replace', 'artifact', 'open', 'reopen', 'kids', 'remote-ai', 'dry-run', 'verify', 'maps', 'profile', 'hands-on', 'automatic', 'unlock', 'confirm', 'no-library', 'no-validate', 'rigged', 'supporter', 'managed', 'live', 'send', 'accept-tos'];
|
|
253
267
|
const positional = [];
|
|
254
268
|
for (let i = 0; i < argv.length; i++) {
|
|
255
269
|
const a = argv[i];
|
|
@@ -461,6 +475,12 @@ function print(result) {
|
|
|
461
475
|
case 'shop disconnect':
|
|
462
476
|
lines.push(...shopLines(result));
|
|
463
477
|
break;
|
|
478
|
+
case 'shop catalog':
|
|
479
|
+
lines.push(...catalogLines(result));
|
|
480
|
+
break;
|
|
481
|
+
case 'setup':
|
|
482
|
+
lines.push(...projectsLines(result));
|
|
483
|
+
break;
|
|
464
484
|
case 'chat rules':
|
|
465
485
|
case 'chat budget':
|
|
466
486
|
case 'shop refund':
|
|
@@ -662,6 +682,12 @@ async function main() {
|
|
|
662
682
|
if (cmd === 'codex') return codexCommand(root, sub);
|
|
663
683
|
if (cmd === 'progress') return progressCommand(root, sub);
|
|
664
684
|
if (cmd === 'setup' && sub === 'attach') return setupAttach(root, positional[2], { homie: flags.get('homie') });
|
|
685
|
+
if (cmd === 'setup' && flags.has('via')) {
|
|
686
|
+
if (flags.get('via') !== 'stripe-projects') return { ok: false, command: 'setup', why: '--via stripe-projects is the one other way this version knows (the default is npx wrangler login)' };
|
|
687
|
+
const extra = String(flags.get('with') ?? '').split(',').map((x) => x.trim()).filter(Boolean);
|
|
688
|
+
if (extra.some((x) => x !== 'elevenlabs')) return { ok: false, command: 'setup', why: '--with takes elevenlabs (Cloudflare is always set up)' };
|
|
689
|
+
return setupViaProjects(root, { withElevenlabs: extra.includes('elevenlabs'), acceptTos: flags.has('accept-tos'), dryRun: flags.has('dry-run'), log });
|
|
690
|
+
}
|
|
665
691
|
if (cmd === 'handoff') return handoff(root, sub, { homie: flags.get('homie') });
|
|
666
692
|
if (cmd === 'port' && sub === 'import') return importPort(root, positional[2], flags.get('id'), { name: flags.get('name'), mode: flags.get('mode') });
|
|
667
693
|
if (cmd === 'port' && sub === 'check') {
|
|
@@ -775,10 +801,22 @@ async function main() {
|
|
|
775
801
|
if (sub === 'check') return shopCheck(root);
|
|
776
802
|
if (sub === 'connect') return shopConnect(root, { managed: flags.has('managed') ? true : null, live: flags.has('live'), log });
|
|
777
803
|
if (sub === 'disconnect') return shopDisconnect(root);
|
|
804
|
+
if (sub === 'catalog') {
|
|
805
|
+
const from = flags.get('have');
|
|
806
|
+
let have = null;
|
|
807
|
+
if (from === '-' || from === true) have = readFileSync(0, 'utf8');
|
|
808
|
+
else if (typeof from === 'string') {
|
|
809
|
+
if (!existsSync(from)) return { ok: false, command: 'shop catalog', why: `no such file: ${from}` };
|
|
810
|
+
have = readFileSync(from, 'utf8');
|
|
811
|
+
}
|
|
812
|
+
const mode = flags.get('mode');
|
|
813
|
+
if (mode !== undefined && mode !== 'test' && mode !== 'live') return { ok: false, command: 'shop catalog', why: '--mode is test or live' };
|
|
814
|
+
return catalogPlan(root, { have, mode: mode ?? null });
|
|
815
|
+
}
|
|
778
816
|
if (sub === 'orders') return shopOrders(root, { url });
|
|
779
817
|
if (sub === 'refund') return shopRefund(root, positional[2], { url, reason: flags.get('reason'), note: flags.get('note') });
|
|
780
818
|
if (sub === 'statements') return shopStatements(root, { url, period: flags.get('period'), send: flags.has('send') });
|
|
781
|
-
return { ok: false, command: 'shop', why: `unknown: shop ${sub} (init, check, connect, disconnect, orders, refund, statements)` };
|
|
819
|
+
return { ok: false, command: 'shop', why: `unknown: shop ${sub} (init, check, connect, disconnect, catalog, orders, refund, statements)` };
|
|
782
820
|
}
|
|
783
821
|
if (cmd === 'agents') {
|
|
784
822
|
const url = flags.get('url');
|
|
@@ -983,7 +1021,7 @@ async function dev(root) {
|
|
|
983
1021
|
const bin = wranglerBin(root);
|
|
984
1022
|
if (!bin) return { ok: false, command: 'dev', why: 'run npm install in the studio first' };
|
|
985
1023
|
const studio = readStudio(root);
|
|
986
|
-
const env = { ...process.env, WRANGLER_SEND_METRICS: 'false', CI: '1' };
|
|
1024
|
+
const env = { ...process.env, ...projectsCloudflareEnv(root), WRANGLER_SEND_METRICS: 'false', CI: '1' };
|
|
987
1025
|
for (const added of ensureMigrations(root)) log(`added ${added} (${migrationWord(added)})`);
|
|
988
1026
|
await new Promise((done) => {
|
|
989
1027
|
const m = spawn(bin, ['d1', 'migrations', 'apply', 'DB', '--local'], { cwd: workerDir(root), env, stdio: ['ignore', 'ignore', 'inherit'] });
|
|
@@ -1065,7 +1103,7 @@ function mediaPut(root, file, as) {
|
|
|
1065
1103
|
const key = as ?? (/^(music|videos)\//.test(rel) && !rel.includes('..') ? rel : `${folder}/${basename(file)}`);
|
|
1066
1104
|
const bin = wranglerBin(root);
|
|
1067
1105
|
return new Promise((done) => {
|
|
1068
|
-
const p = spawn(bin, ['r2', 'object', 'put', `${r2}/${key}`, '--file', resolve(file), '--content-type', typeOf(file), '--remote'], { cwd: workerDir(root), env: { ...process.env, CI: '1' }, stdio: ['ignore', 'ignore', 'inherit'] });
|
|
1106
|
+
const p = spawn(bin, ['r2', 'object', 'put', `${r2}/${key}`, '--file', resolve(file), '--content-type', typeOf(file), '--remote'], { cwd: workerDir(root), env: { ...process.env, ...projectsCloudflareEnv(root), CI: '1' }, stdio: ['ignore', 'ignore', 'inherit'] });
|
|
1069
1107
|
p.on('close', (code) => {
|
|
1070
1108
|
if (code !== 0) return done({ ok: false, command: 'media put', why: 'wrangler r2 object put failed' });
|
|
1071
1109
|
// The manifest is committed: it names the file by its path on the site, never the workers.dev address.
|
package/lib/cloudflare.mjs
CHANGED
|
@@ -30,6 +30,7 @@ import { MAX_ASSET_BYTES, R2_COST, mediaPlan, r2OverOf, recordMove, sha256File,
|
|
|
30
30
|
import { repoOf } from './repo.mjs';
|
|
31
31
|
import { ensureLocalIgnored, ensureMigrations, migrationWord, wranglerConfig } from './scaffold.mjs';
|
|
32
32
|
import { LOCAL_STATE, configPath, isWorkersDev, layoutOf, readLocal, readStudio, siteUrl, workerDir, writeLocal, writeStudio } from './studio.mjs';
|
|
33
|
+
import { projectsCloudflareEnv } from './projects-env.mjs';
|
|
33
34
|
|
|
34
35
|
const ANSI = /\u001b\[[0-9;]*m/g;
|
|
35
36
|
|
|
@@ -38,13 +39,17 @@ export function wranglerBin(root) {
|
|
|
38
39
|
return existsSync(local) ? local : null;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Wrangler from the studio's own node_modules, run in site/ with the given environment. A studio whose Cloudflare came
|
|
44
|
+
* through Stripe Projects (studio.json `cloudflare.auth: "stripe-projects"`, 0.24.3) runs it with the token and account
|
|
45
|
+
* Projects synced to its git-ignored output file; every other studio with Wrangler's own sign-in.
|
|
46
|
+
*/
|
|
42
47
|
export function runner(root, env) {
|
|
43
48
|
const bin = wranglerBin(root);
|
|
44
49
|
if (!bin) throw new Error('Wrangler is not installed in this studio yet: run `npm install` in the studio folder first');
|
|
45
50
|
return (args, { cwd = workerDir(root), input } = {}) => {
|
|
46
51
|
const res = spawnSync(bin, args, {
|
|
47
|
-
cwd, env: { ...process.env, ...env, WRANGLER_SEND_METRICS: 'false', CI: '1' }, encoding: 'utf8',
|
|
52
|
+
cwd, env: { ...process.env, ...projectsCloudflareEnv(root), ...env, WRANGLER_SEND_METRICS: 'false', CI: '1' }, encoding: 'utf8',
|
|
48
53
|
input, maxBuffer: 64 * 1024 * 1024, timeout: 10 * 60_000,
|
|
49
54
|
});
|
|
50
55
|
const out = `${res.stdout ?? ''}${res.stderr ?? ''}`.replace(ANSI, '');
|
|
@@ -128,6 +133,9 @@ export async function deploy(root, { log = () => {}, homie } = {}) {
|
|
|
128
133
|
const created = new Set(cf.created ?? []);
|
|
129
134
|
const who = whoami(root);
|
|
130
135
|
if (!who) {
|
|
136
|
+
if (cf.auth === 'stripe-projects') {
|
|
137
|
+
return { ok: false, command: 'deploy', needs: 'cloudflare-login', why: 'Wrangler did not accept the Cloudflare token Stripe Projects keeps for this studio (studio.json "auth": "stripe-projects"). Run `stripe projects env --pull` in the studio folder (it rewrites the git-ignored .env from Projects\' vault), then `npm run deploy` again; or sign in the usual way (`npx wrangler login`) and remove "auth" from studio.json.' };
|
|
138
|
+
}
|
|
131
139
|
return { ok: false, command: 'deploy', needs: 'cloudflare-login', why: 'Wrangler is not signed in to Cloudflare. Run `npx wrangler login` in the studio folder: it opens Cloudflare in the browser and the person approves once (a free account works, no payment method). Then run `npm run deploy` again.' };
|
|
132
140
|
}
|
|
133
141
|
const accountId = process.env.CLOUDFLARE_ACCOUNT_ID || cf.accountId || (who.accounts.length === 1 ? who.accounts[0].id : null);
|
|
@@ -490,7 +498,7 @@ export async function mediaMove(root, { paths = null, dryRun = false, verify = f
|
|
|
490
498
|
const bin = wranglerBin(root);
|
|
491
499
|
if (!bin) return { ok: false, ...base, why: 'Wrangler is not installed in this studio yet: run `npm install` in the studio folder first' };
|
|
492
500
|
const account = accountId || process.env.CLOUDFLARE_ACCOUNT_ID || cf.accountId || null;
|
|
493
|
-
const opts = { cwd: workerDir(root), env: { ...process.env, ...(account ? { CLOUDFLARE_ACCOUNT_ID: account } : {}), WRANGLER_SEND_METRICS: 'false', CI: '1' } };
|
|
501
|
+
const opts = { cwd: workerDir(root), env: { ...process.env, ...projectsCloudflareEnv(root), ...(account ? { CLOUDFLARE_ACCOUNT_ID: account } : {}), WRANGLER_SEND_METRICS: 'false', CI: '1' } };
|
|
494
502
|
const moved = [];
|
|
495
503
|
const failed = [];
|
|
496
504
|
const verified = [];
|
package/lib/doctor.mjs
CHANGED
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
* GitHub optional a private backup, publishing by pull request, building from the Claude app
|
|
17
17
|
* ElevenLabs optional songs and game scores
|
|
18
18
|
* fal optional painted art and generated video
|
|
19
|
+
* Stripe for selling only in a studio with shop.json (0.24.3): Stripe's own agent plugin (its MCP server
|
|
20
|
+
* and skills) for this AI, set up when the studio starts selling; the shop's key itself
|
|
21
|
+
* goes in through `homie-studio shop connect`, never here
|
|
19
22
|
* status line optional (Claude Code only) the build's progress under the prompt
|
|
20
23
|
*
|
|
21
24
|
* It is safe at any time, inside a studio or before one exists: it only reads (local files, `--version` of a few
|
|
@@ -32,6 +35,7 @@ import { findChrome } from './chrome.mjs';
|
|
|
32
35
|
import { whyFailed } from './net.mjs';
|
|
33
36
|
import { isOurs } from './statusline.mjs';
|
|
34
37
|
import { findStudio, readLocal, readStudio } from './studio.mjs';
|
|
38
|
+
import { projectsCloudflareEnv, projectsElevenLabs } from './projects-env.mjs';
|
|
35
39
|
import { STUDIO_VERSION } from './version.mjs';
|
|
36
40
|
|
|
37
41
|
/** A command's result, never throwing: code 127 when the program is not installed, 124 when it ran out of time. */
|
|
@@ -101,7 +105,12 @@ export async function setupStatus({
|
|
|
101
105
|
if (env.CLOUDFLARE_API_TOKEN) return { signedIn: true, how: 'an API token in the environment' };
|
|
102
106
|
const bin = root ? join(root, 'node_modules', '.bin', win ? 'wrangler.cmd' : 'wrangler') : null;
|
|
103
107
|
if (!bin || !existsSync(bin)) return { signedIn: null };
|
|
104
|
-
|
|
108
|
+
// Through Stripe Projects (studio.json "auth": "stripe-projects"): Wrangler with the token Projects synced.
|
|
109
|
+
const projects = projectsCloudflareEnv(root);
|
|
110
|
+
const r = await exec(bin, ['whoami', '--json'], { cwd: root, timeout: 25_000, env: { ...env, ...projects, WRANGLER_SEND_METRICS: 'false', CI: '1' } });
|
|
111
|
+
if (projects.CLOUDFLARE_API_TOKEN && r.code === 0) {
|
|
112
|
+
try { if (JSON.parse(r.stdout.slice(Math.max(0, r.stdout.indexOf('{')))).loggedIn) return { signedIn: true, how: 'through Stripe Projects (its token, in the studio\'s git-ignored .env; never shown)', accounts: 1, accountIds: [projects.CLOUDFLARE_ACCOUNT_ID].filter(Boolean) }; } catch { /* read below */ }
|
|
113
|
+
}
|
|
105
114
|
if (r.code === 124) return { signedIn: null, why: 'Wrangler did not answer in time' };
|
|
106
115
|
// The accounts' ids stay in this process (the Workers AI row calls the studio's own account); never printed.
|
|
107
116
|
try { const d = JSON.parse(r.stdout.slice(Math.max(0, r.stdout.indexOf('{')))); return { signedIn: Boolean(d.loggedIn), accounts: Array.isArray(d.accounts) ? d.accounts.length : null, accountIds: Array.isArray(d.accounts) ? d.accounts.map((a) => a?.id).filter(Boolean) : [] }; } catch { return { signedIn: false }; }
|
|
@@ -124,7 +133,7 @@ export async function setupStatus({
|
|
|
124
133
|
const s = await exec(cliBin, ['auth', 'status', '--format', 'json'], { timeout: 10_000 });
|
|
125
134
|
try { signedIn = Boolean(JSON.parse(s.stdout)?.schemes?.some((x) => x.logged_in)); } catch { signedIn = false; }
|
|
126
135
|
}
|
|
127
|
-
return { cli: v.code === 0, signedIn, key: Boolean(env.ELEVENLABS_API_KEY) };
|
|
136
|
+
return { cli: v.code === 0, signedIn, key: Boolean(env.ELEVENLABS_API_KEY), projects: root ? projectsElevenLabs(root) : false };
|
|
128
137
|
})(),
|
|
129
138
|
(async () => {
|
|
130
139
|
const key = String(env.FAL_KEY ?? '').trim();
|
|
@@ -231,10 +240,10 @@ export async function setupStatus({
|
|
|
231
240
|
|
|
232
241
|
// ElevenLabs.
|
|
233
242
|
rows.push({
|
|
234
|
-
id: 'elevenlabs', label: 'ElevenLabs', need: 'optional', state: eleven.signedIn || eleven.key ? 'ok' : 'optional',
|
|
235
|
-
detail: eleven.signedIn ? 'the elevenlabs CLI is signed in' : eleven.key ? 'ELEVENLABS_API_KEY is set' : eleven.cli ? 'the elevenlabs CLI is here, not signed in' : 'not connected',
|
|
243
|
+
id: 'elevenlabs', label: 'ElevenLabs', need: 'optional', state: eleven.signedIn || eleven.key || eleven.projects ? 'ok' : 'optional',
|
|
244
|
+
detail: eleven.signedIn ? 'the elevenlabs CLI is signed in' : eleven.key ? 'ELEVENLABS_API_KEY is set' : eleven.projects ? 'through Stripe Projects (its key, in the studio\'s git-ignored .env; never shown)' : eleven.cli ? 'the elevenlabs CLI is here, not signed in' : 'not connected',
|
|
236
245
|
unlocks: 'songs and game scores (the music skill), billed to your own ElevenLabs plan; every render is quoted first',
|
|
237
|
-
fix: eleven.signedIn || eleven.key ? null : eleven.cli
|
|
246
|
+
fix: eleven.signedIn || eleven.key || eleven.projects ? null : eleven.cli
|
|
238
247
|
? { who: 'ai', run: 'elevenlabs auth login', say: 'ElevenLabs opens in your browser; sign in once. No key is pasted anywhere.' }
|
|
239
248
|
: { who: mac ? 'ai' : 'person', run: mac ? 'brew install elevenlabs/tap/elevenlabs && elevenlabs auth login' : undefined, open: 'https://elevenlabs.io/sign-up', say: 'Make an ElevenLabs account when you want songs; your AI installs their CLI and you sign in once in the browser.' },
|
|
240
249
|
});
|
|
@@ -247,6 +256,24 @@ export async function setupStatus({
|
|
|
247
256
|
fix: fal.key && fal.valid !== false ? null : { who: 'person', open: 'https://fal.ai/dashboard/keys', say: 'Make a key, put FAL_KEY in the environment your AI runs in (for example a line in your shell profile), and start a new session. Never paste it into the chat.' },
|
|
248
257
|
});
|
|
249
258
|
|
|
259
|
+
// Stripe, only in a studio that sells (shop.json): Stripe's own agent plugin, set up when selling starts (lazy, like
|
|
260
|
+
// every provider here). The shop's key never comes through here: `homie-studio shop connect` takes it on a page.
|
|
261
|
+
if (root && existsSync(join(root, 'shop.json'))) {
|
|
262
|
+
const v = await exec('stripe', ['--version']);
|
|
263
|
+
const version = v.code === 0 ? /(\d+\.\d+\.\d+)/.exec(v.stdout)?.[1] ?? null : null;
|
|
264
|
+
const mcp = stripeAgentConfigured({ root, env });
|
|
265
|
+
rows.push({
|
|
266
|
+
id: 'stripe', label: 'Stripe', need: 'for selling', state: mcp ? 'ok' : 'act',
|
|
267
|
+
detail: [mcp ? `Stripe's MCP is set up for ${mcp}` : 'Stripe\'s MCP is not set up for this AI', version ? `the Stripe CLI ${version}` : 'no Stripe CLI'].join('; '),
|
|
268
|
+
unlocks: 'the shop: your AI makes the catalog, checks tax and Managed Payments and answers "how are sales?" with Stripe\'s own tools, on your own Stripe account; you sign in on Stripe\'s page',
|
|
269
|
+
fix: mcp ? null : {
|
|
270
|
+
who: 'ai', run: 'npm install -g @stripe/cli@latest && stripe agent setup',
|
|
271
|
+
say: 'Stripe\'s own agent plugin for Claude Code and Codex (its MCP server and skills, kept up to date); you approve the install. Then sign in once on Stripe\'s page and give access to your Stripe sandbox first.',
|
|
272
|
+
open: 'https://dashboard.stripe.com/register',
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
250
277
|
// The Claude Code status line (only in Claude Code, only in a studio).
|
|
251
278
|
if (root && env.CLAUDECODE === '1') {
|
|
252
279
|
let on = false;
|
|
@@ -270,6 +297,7 @@ export async function setupStatus({
|
|
|
270
297
|
{ feature: 'Songs and game scores', state: ready(by.elevenlabs.state), needs: ['elevenlabs'] },
|
|
271
298
|
{ feature: 'Painted art and generated video', state: ready(by.fal.state), needs: ['fal'] },
|
|
272
299
|
...(by['workers-ai'] ? [{ feature: 'AI guides that think (Workers AI)', state: ready(by['workers-ai'].state), needs: ['workers-ai'] }] : []),
|
|
300
|
+
...(by.stripe ? [{ feature: 'Sell in the games (the shop, with Stripe\'s own tools)', state: ready(by.stripe.state), needs: ['stripe'] }] : []),
|
|
273
301
|
];
|
|
274
302
|
const now = rows.filter((r) => ['missing', 'act'].includes(r.state) && r.fix);
|
|
275
303
|
const meanwhile = rows.filter((r) => r.fix?.open && r.state !== 'ok' && (r.fix.who === 'person' || r.state === 'later'));
|
|
@@ -284,6 +312,23 @@ export async function setupStatus({
|
|
|
284
312
|
};
|
|
285
313
|
}
|
|
286
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Where Stripe's MCP server (https://mcp.stripe.com) is set up for this person's AI: Claude Code (the Stripe plugin,
|
|
317
|
+
* or an MCP entry in .claude.json in the home folder, or the studio's .mcp.json) or Codex (config.toml in the Codex
|
|
318
|
+
* home folder). It only looks for the address or the plugin's name in those files; it never reads a key and never
|
|
319
|
+
* prints what it found. Null when nowhere.
|
|
320
|
+
*/
|
|
321
|
+
export function stripeAgentConfigured({ root, env = process.env } = {}) {
|
|
322
|
+
const home = env.HOME || env.USERPROFILE || '';
|
|
323
|
+
const look = (file, re) => {
|
|
324
|
+
try { if (!file || !existsSync(file)) return false; const t = readFileSync(file, 'utf8'); return t.length < 64 * 1024 * 1024 && re.test(t); } catch { return false; }
|
|
325
|
+
};
|
|
326
|
+
const mcp = /mcp\.stripe\.com/;
|
|
327
|
+
if (look(home && join(home, '.claude', 'plugins', 'installed_plugins.json'), /"stripe@/) || look(home && join(home, '.claude.json'), mcp) || look(root && join(root, '.mcp.json'), mcp)) return 'Claude Code';
|
|
328
|
+
if (look(join(env.CODEX_HOME || (home && join(home, '.codex')) || '', 'config.toml'), /mcp\.stripe\.com|\[mcp_servers\.stripe\]|stripe@openai-curated/)) return 'Codex';
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
|
|
287
332
|
/** The checklist as a person reads it in a terminal. */
|
|
288
333
|
export function formatStatus(r) {
|
|
289
334
|
const lines = [`Setup status${r.studio?.name ? ` for ${r.studio.name}` : ' (before the studio exists)'}`, ''];
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The credentials Stripe Projects keeps for a studio (`homie-studio setup --via stripe-projects`, 0.24.3), read only
|
|
3
|
+
* where they are used and never printed, returned to a person, or written anywhere else.
|
|
4
|
+
*
|
|
5
|
+
* Stripe Projects keeps them encrypted in its vault and syncs them to the project's output file (`.env` unless the
|
|
6
|
+
* environment names another), which it creates readable by this user only and git-ignores. Wrangler then deploys with
|
|
7
|
+
* the Cloudflare token from that file instead of a `wrangler login`; the music skill reads the ElevenLabs key the same
|
|
8
|
+
* way. studio.json says which (never a value): `cloudflare.auth: "stripe-projects"`, `providers.elevenlabs.via`.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
11
|
+
import { isAbsolute, join, normalize, sep } from 'node:path';
|
|
12
|
+
|
|
13
|
+
/** The output file inside the studio (`.env` by default); null for anything outside it. */
|
|
14
|
+
export function envFileOf(root, name = '.env') {
|
|
15
|
+
const n = normalize(String(name || '.env'));
|
|
16
|
+
if (isAbsolute(n) || n === '..' || n.startsWith(`..${sep}`)) return null;
|
|
17
|
+
return join(root, n);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const unquote = (v) => {
|
|
21
|
+
if (v.length >= 2 && v.startsWith('"') && v.endsWith('"')) { try { return JSON.parse(v); } catch { return v.slice(1, -1); } }
|
|
22
|
+
if (v.length >= 2 && v.startsWith("'") && v.endsWith("'")) return v.slice(1, -1);
|
|
23
|
+
return v;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** A dotenv file as { NAME: value } (KEY=value lines; `export ` and comments allowed). {} when absent. */
|
|
27
|
+
export function readDotenv(file) {
|
|
28
|
+
const out = {};
|
|
29
|
+
if (!file || !existsSync(file)) return out;
|
|
30
|
+
let text = '';
|
|
31
|
+
try { text = readFileSync(file, 'utf8'); } catch { return out; }
|
|
32
|
+
for (const line of text.split(/\r?\n/)) {
|
|
33
|
+
const t = line.trim().replace(/^export\s+/, '');
|
|
34
|
+
if (!t || t.startsWith('#')) continue;
|
|
35
|
+
const i = t.indexOf('=');
|
|
36
|
+
if (i < 1) continue;
|
|
37
|
+
const k = t.slice(0, i).trim();
|
|
38
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(k)) continue;
|
|
39
|
+
out[k] = unquote(t.slice(i + 1).trim());
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function studioOf(root) {
|
|
45
|
+
try { return JSON.parse(readFileSync(join(root, 'studio.json'), 'utf8')); } catch { return null; }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The environment Wrangler gets for a studio whose Cloudflare came through Stripe Projects: CLOUDFLARE_API_TOKEN and
|
|
50
|
+
* CLOUDFLARE_ACCOUNT_ID from the Projects output file. {} for every other studio (the default: `wrangler login`).
|
|
51
|
+
*/
|
|
52
|
+
export function projectsCloudflareEnv(root, studio = studioOf(root)) {
|
|
53
|
+
const cf = studio?.cloudflare;
|
|
54
|
+
if (!root || cf?.auth !== 'stripe-projects') return {};
|
|
55
|
+
const vars = readDotenv(envFileOf(root, cf.envFile));
|
|
56
|
+
return {
|
|
57
|
+
...(vars.CLOUDFLARE_API_TOKEN ? { CLOUDFLARE_API_TOKEN: vars.CLOUDFLARE_API_TOKEN } : {}),
|
|
58
|
+
...(vars.CLOUDFLARE_ACCOUNT_ID ? { CLOUDFLARE_ACCOUNT_ID: vars.CLOUDFLARE_ACCOUNT_ID } : {}),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Whether Stripe Projects holds this studio's ElevenLabs key (studio.json `providers.elevenlabs`); never the key. */
|
|
63
|
+
export function projectsElevenLabs(root, studio = studioOf(root)) {
|
|
64
|
+
const p = studio?.providers?.elevenlabs;
|
|
65
|
+
if (!root || p?.via !== 'stripe-projects') return false;
|
|
66
|
+
const name = /^[A-Z][A-Z0-9_]{2,}$/.test(String(p.envKey ?? '')) ? p.envKey : 'ELEVENLABS_API_KEY';
|
|
67
|
+
return Boolean(readDotenv(envFileOf(root, p.envFile))[name]);
|
|
68
|
+
}
|