@mediamonks/canopy-cli 0.4.5 → 0.5.1

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.
Files changed (37) hide show
  1. package/README.md +43 -0
  2. package/dist/{db-PpaU3V-B.mjs → db-BW1ZolWn.mjs} +8 -11
  3. package/dist/{deploy-B064KXtr.mjs → deploy-alC2--pZ.mjs} +92 -12
  4. package/dist/dev-BJ5xEwqW.mjs +2 -0
  5. package/dist/{dev-CuvlJpcO.mjs → dev-CQQhMOoD.mjs} +22 -24
  6. package/dist/docs/ci.md +6 -2
  7. package/dist/docs/cli.md +8 -3
  8. package/dist/docs/data.md +22 -4
  9. package/dist/docs/deploy.md +10 -6
  10. package/dist/docs/dev.md +10 -3
  11. package/dist/docs/infrastructure.md +397 -0
  12. package/dist/docs/static-serving.md +318 -0
  13. package/dist/docs/troubleshooting.md +14 -0
  14. package/dist/{docs-UYzmzpz-.mjs → docs-DvSPpSIR.mjs} +8 -7
  15. package/dist/index.mjs +53 -19
  16. package/dist/local-C5-GWJcx.mjs +826 -0
  17. package/dist/login-BO6abf40.mjs +383 -0
  18. package/dist/{open-CBuZBJlv.mjs → open-DnlYDqrU.mjs} +2 -2
  19. package/dist/{package-Bnu23N0A.mjs → package-CHoZKzaw.mjs} +1 -1
  20. package/dist/{projects-CK4DNxgZ.mjs → projects-ijfe7Ffx.mjs} +21 -3
  21. package/dist/site-BJjoePQS.mjs +254 -0
  22. package/dist/skill/canopy/SKILL.md +1 -1
  23. package/dist/{skill-h7QlCMhc.mjs → skill-CW0QSbAJ.mjs} +2 -2
  24. package/dist/templates/static/AGENTS.md +1 -1
  25. package/dist/templates/static/main.js +1 -1
  26. package/dist/templates/worker/AGENTS.md +11 -9
  27. package/dist/templates/worker/worker/index.ts +27 -4
  28. package/dist/{token-yuwQTQB8.mjs → token-BeF9zczg.mjs} +2 -2
  29. package/dist/{ui-D1DXoDN1.mjs → ui-DThlyHgo.mjs} +28 -36
  30. package/dist/{upgrade-sXoqYLRu.mjs → upgrade-CGz1yzsW.mjs} +3 -3
  31. package/dist/usage-DQpH7A3X.mjs +79 -0
  32. package/package.json +4 -3
  33. package/dist/dev-BYv6BmcI.mjs +0 -2
  34. package/dist/local-B3dyT2fh.mjs +0 -563
  35. package/dist/login-DdNc9OjU.mjs +0 -173
  36. package/dist/project-DKbgJACM.mjs +0 -54
  37. package/dist/{version-check-CoWgQs2h.mjs → version-check-Cuh6HaLX.mjs} +1 -1
@@ -0,0 +1,397 @@
1
+ # Infrastructure as code & standing up a Cloudflare account
2
+
3
+ Canopy's Cloudflare footprint is small on purpose: almost everything
4
+ per-project is provisioned at runtime by canopy-api (`apps/api/src/cf.ts`,
5
+ reached through the Cloudflare `PlatformAdapter` in
6
+ `apps/api/src/platform/cloudflare.ts`), so the only things worth managing
7
+ declaratively are a handful of platform-wide resources. Those live in
8
+ [`infra/`](../../infra) as OpenTofu; the rest is bootstrap scripts and this
9
+ runbook.
10
+
11
+ **Live platform (2026-08-01):** Monks Explore account
12
+ `1c826c7b063fd9ca5e1b1bdaabd05feb`, zone `canopy.website`. The steps below
13
+ are the verified path used for that move (personal account
14
+ `9513cbfa5b182546f737501b10e1ed85` / `thanarie.dev` → company). Re-run them
15
+ to stand the platform up in another fresh account, or to rebuild after a
16
+ wipe.
17
+
18
+ Scope of a fresh stand-up:
19
+
20
+ 1. the **platform workers** — `canopy-api` and `canopy-gateway` — deployed and
21
+ healthy (these aren't optional; nothing works without them), and
22
+ 2. the **three core projects** — `dashboard`, `www`, `kitchen-sink` — re-created
23
+ and deployed _on_ the platform like any other project.
24
+
25
+ Everyone else's projects are **not** carried over — a new account starts with
26
+ an empty control-plane registry (see [Non-goals](#non-goals)).
27
+
28
+ See decision **D14** in [`02-decisions.md`](../02-decisions.md) for the why.
29
+
30
+ ## What's code vs. what's clicked
31
+
32
+ **OpenTofu** (`infra/*.tf`): zone, proxied wildcard DNS record, control-plane
33
+ D1, R2 bucket, AI Gateway shell, optional ACM cert pack.
34
+
35
+ **wrangler** (`apps/{api,gateway}/wrangler.{main,staging}.jsonc`): the two
36
+ platform Worker scripts, their bindings, and the `*.<zone>/*` route — one
37
+ config file per Cloudflare site (same names as CLI profiles). Deployed with
38
+ `pnpm run deploy:main` / `deploy:staging`, not Tofu. Plain `wrangler.jsonc`
39
+ is local `wrangler dev` only.
40
+
41
+ **`canopy deploy`** (`canopy.json` in each project): the core projects
42
+ (`dashboard`, `www`, `kitchen-sink`) — ordinary platform projects that
43
+ dogfood the product, not infrastructure.
44
+
45
+ **Bootstrap scripts** (`infra/scripts/`): runtime API tokens, worker secrets,
46
+ AI Gateway BYOK keys + spend rule.
47
+
48
+ **Manual / billing (clicked once in the dashboard):** create the account &
49
+ attach it to the org; add the domain; mint bootstrap API token(s); optionally
50
+ enable paid Workers products. Plus OneLogin: redirect URIs for the new host
51
+ (if they aren't already set).
52
+
53
+ ## Non-goals
54
+
55
+ - **No project-data migration.** Per-project D1s, R2 blobs, and deploy history
56
+ stay in the old account. The new control-plane D1 is empty; the three core
57
+ projects are re-created from scratch (they hold no precious data — `www` and
58
+ `dashboard` are stateless, `kitchen-sink` is a demo), and everyone else's
59
+ projects are re-created as needed. (If that ever changes, the export/import
60
+ primitives already exist in `apps/api/src/cf.ts` and `canopy db snapshot|restore`.)
61
+ - **No Tofu management of Workers/secrets/per-project resources** — see the
62
+ table in [`infra/README.md`](../../infra/README.md).
63
+ - **No per-project Workers on the Cloudflare target today.** The CF
64
+ `PlatformAdapter` reports `capabilities.workers = false` (and
65
+ schedules/secrets likewise). Project backends, schedules, and project
66
+ secrets live on targets that support them (e.g. GCP). Kitchen-sink's
67
+ `worker` field in `canopy.json` must be omitted for a CF deploy — see
68
+ [step 10](#10-log-in-grant-yourself-admin-then-deploy-the-core-projects).
69
+
70
+ ---
71
+
72
+ ## Rollout — step by step
73
+
74
+ Prereqs on your machine: `tofu` (or `terraform`), `wrangler` (via `pnpm` in
75
+ the repo), `curl`, `jq`, `openssl`. Confirm wrangler can see the target
76
+ account after `wrangler login`:
77
+
78
+ ```bash
79
+ pnpm --filter api exec wrangler whoami
80
+ # expect the new account listed (e.g. "Monks Explore")
81
+ export CLOUDFLARE_ACCOUNT_ID="<account_id>" # required when you belong to >1 account
82
+ ```
83
+
84
+ ### 1. Account & billing (dashboard)
85
+
86
+ 1. Create / choose the new account; attach it to the company org; ensure your
87
+ user is a member with rights to manage Workers / D1 / R2 / DNS.
88
+ 2. Add `canopy.website` to the account and repoint its nameservers at
89
+ Cloudflare. Wait for the zone to go **Active**. Note the **Account ID**
90
+ (`tofu` var `account_id`) and the **Zone ID**.
91
+ 3. **Delete any auto-created `www` record.** Cloudflare's onboarding scan often
92
+ adds a `www` A/CNAME. An explicit record beats the wildcard, and if it's
93
+ DNS-only (grey cloud) the request never reaches Workers — which would break
94
+ the `www` core project. The wildcard `*` alone must serve `www.canopy.website`.
95
+ (You can confirm via API once the bootstrap token exists — an empty zone is
96
+ fine.)
97
+ 4. **R2** must work (creating a probe bucket is enough to confirm). **Workers
98
+ Paid** and **Workers for Platforms** are _not_ required for the current CF
99
+ platform path (no dispatch namespaces; `capabilities.workers` is false).
100
+ Enable them later when you actually need paid Worker limits or Phase-2
101
+ per-project Workers. **No Advanced Certificate Manager** — `*.canopy.website`
102
+ is a single-level wildcard covered by free Universal SSL. (ACM would only
103
+ be needed if nesting as `*.canopy.<some-other-domain>`.)
104
+
105
+ > **Apex is not covered.** `*` matches only one label, so `canopy.website`
106
+ > itself isn't served by the gateway (by design — see
107
+ > `apps/gateway/wrangler.main.jsonc`).
108
+ > Decide separately whether the apex should redirect to `www.canopy.website`
109
+ > (a Cloudflare Redirect Rule, cheap to add later); nothing in this runbook
110
+ > depends on it.
111
+
112
+ ### 2. Bootstrap API token(s) (dashboard)
113
+
114
+ You need credentials that can (a) run OpenTofu + wrangler ops, and (b) mint
115
+ the least-privilege runtime tokens. In practice that is often **two**
116
+ account-owned tokens — a single token that mixes zone DNS with **Account API
117
+ Tokens · Write** has been flaky when edited in place.
118
+
119
+ **Token A — `canopy-bootstrap` (Tofu + deploy ops)**
120
+
121
+ | Resource | Permissions |
122
+ | ------------------------------------------------------------ | ----------------------------------------------------------------------- |
123
+ | Account | D1 Edit, Workers R2 Storage Edit, AI Gateway Edit, Workers Scripts Edit |
124
+ | Zone `canopy.website` (nested under the account — see below) | Zone Edit, DNS Edit, Workers Routes Edit |
125
+
126
+ **Token B — mint token (for `create-api-tokens.sh` only)**
127
+
128
+ | Resource | Permissions |
129
+ | -------- | ----------------------------------------- |
130
+ | Account | **Account API Tokens · Write** (and Read) |
131
+
132
+ **Pitfalls we hit:**
133
+
134
+ - The dashboard's "DNS" picks are easy to get wrong. You need zone-scoped
135
+ **DNS Write** / **Zone Write**, _not_ Account DNS Settings, DNS Views,
136
+ DNS Firewall, or Registrar Domains.
137
+ - For **account-owned** tokens, zone resources must be **nested under the
138
+ account** in the policy JSON shape
139
+ `com.cloudflare.api.account.<account_id> → com.cloudflare.api.account.zone.<zone_id>`.
140
+ A top-level zone resource can look correct in the UI and still 10000-auth on
141
+ `/zones/:id/dns_records`.
142
+ - `/user/tokens/verify` returns "Invalid API Token" for account-owned tokens —
143
+ use `GET /accounts/<account_id>/tokens/verify` instead.
144
+ - Wrangler **OAuth** can list/create D1 and R2 and deploy Workers, but it
145
+ cannot manage DNS (and often cannot call AI Gateway). Don't rely on OAuth
146
+ alone for step 3.
147
+
148
+ Store tokens outside git. Convenience pattern used in the move:
149
+
150
+ ```bash
151
+ cd infra
152
+ # gitignored via infra/.gitignore (.env / .env.*)
153
+ cat > .env <<'EOF'
154
+ CLOUDFLARE_API_TOKEN="<token A — canopy-bootstrap>"
155
+ CLOUDFLARE_TOKEN_MINT_TOKEN="<token B — API Tokens Write>"
156
+ EOF
157
+ set -a && source .env && set +a
158
+ ```
159
+
160
+ ### 3. Provision static infra (OpenTofu)
161
+
162
+ ```bash
163
+ cd infra
164
+ cp terraform.tfvars.example terraform.tfvars # set account_id + zone_name
165
+ set -a && source .env && set +a # CLOUDFLARE_API_TOKEN = token A
166
+ tofu init
167
+ tofu import cloudflare_zone.platform <zone_id> # adopt the domain from step 1
168
+ tofu plan # review — should be creates only
169
+ tofu apply
170
+ tofu output wrangler_patch_hint
171
+ ```
172
+
173
+ Expect four creates: wildcard DNS `*`, D1 `canopy-control`, R2 `canopy-assets`,
174
+ AI Gateway `canopy`. (If the AI Gateway resource fails schema validation,
175
+ check `cache_invalidate_on_update` and friends against the pinned
176
+ `cloudflare` provider minor — it shifted in ~> 5.22.)
177
+
178
+ The R2 remote-state backend in `versions.tf` is commented out; for a solo
179
+ stand-up, local state is fine. If you want shared state, create a
180
+ `canopy-tfstate` bucket first, then uncomment and `tofu init -migrate-state`.
181
+
182
+ ### 4. Point the apps at the new resources
183
+
184
+ Edit the **site** wrangler configs (not the local `wrangler.jsonc`) using
185
+ `tofu output wrangler_patch_hint`. For production that is
186
+ `wrangler.main.jsonc` in both apps; for the personal/staging account use
187
+ `wrangler.staging.jsonc`. Keep both files in git — never comment-flip a
188
+ single file to switch accounts.
189
+
190
+ **`apps/api/wrangler.<site>.jsonc`:**
191
+
192
+ - `account_id` / `vars.CF_ACCOUNT_ID` → account id
193
+ - `d1_databases[0].database_id` → control-plane D1 UUID
194
+ - `vars.AI_GATEWAY_ID` → unchanged (`canopy`) unless renamed
195
+ - `r2_buckets[0].bucket_name` → unchanged (`canopy-assets`) unless renamed
196
+
197
+ **`apps/gateway/wrangler.<site>.jsonc`:**
198
+
199
+ - `account_id` → same account id
200
+ - `routes[0]` → `{ "pattern": "*.canopy.website/*", "zone_name": "canopy.website" }`
201
+ (or `*.thanarie.dev/*` for staging)
202
+ - `vars.COOKIE_DOMAIN` / `AUTH_HOST` / `API_HOST` → that site's hosts
203
+
204
+ Also point the CLI at the site (`canopy-dev --profile main` / `staging`, or
205
+ `CANOPY_API_URL=https://api.<domain>`). Until the workers exist, skip the
206
+ CLI bits and come back in step 10.
207
+
208
+ ### 5. Runtime tokens (script)
209
+
210
+ Mint with **token B** (API Tokens · Write). Token A cannot create other
211
+ tokens if it lacks that scope (and a token that _has_ Tokens·Write cannot
212
+ always be used as a "sub-token" parent for another Tokens·Write policy):
213
+
214
+ ```bash
215
+ cd infra
216
+ set -a && source .env && set +a
217
+ export CLOUDFLARE_API_TOKEN="$CLOUDFLARE_TOKEN_MINT_TOKEN"
218
+ CANOPY_ACCOUNT_ID="$(tofu output -raw account_id)" \
219
+ ./scripts/create-api-tokens.sh | tee .env.runtime
220
+ ```
221
+
222
+ Prints `CF_API_TOKEN` and `AI_GATEWAY_TOKEN` once. Keep them for secrets
223
+ (`.env.runtime` is gitignored via `.env.*`).
224
+
225
+ ### 6. Apply the control-plane D1 schema
226
+
227
+ The Tofu-created D1 is empty. Load the schema into the remote (production) DB —
228
+ the `db:setup` script targets `--local`, so pass `--remote` explicitly:
229
+
230
+ ```bash
231
+ cd ../apps/api
232
+ # Use the site config that matches the account (main = canopy.website).
233
+ pnpm exec wrangler d1 execute canopy-control --remote --file schema.sql \
234
+ -c wrangler.main.jsonc
235
+ ```
236
+
237
+ Do **not** run `seed.local.sql` against remote — it's local dev seed data. Your
238
+ user row doesn't exist yet either; it's auto-provisioned on first login, and
239
+ admin is granted in step 10.
240
+
241
+ ### 7. Deploy the platform workers
242
+
243
+ `wrangler secret put` needs the Worker script to exist, so deploy **before**
244
+ setting secrets. Use each package's `run deploy` script — bare
245
+ `pnpm --filter api deploy` is pnpm's own `deploy` command and will fail with
246
+ `ERR_PNPM_INVALID_DEPLOY_TARGET`:
247
+
248
+ ```bash
249
+ # from repo root — pick the site (deploy aliases deploy:main)
250
+ pnpm --filter api run deploy:main # or deploy:staging
251
+ pnpm --filter gateway run deploy:main # same site
252
+ ```
253
+
254
+ Sanity check: `curl -I https://auth.canopy.website` should 302 toward
255
+ `/auth/login` (gateway alive). `curl -I https://auth.canopy.website/auth/login`
256
+ should 302 to OneLogin with
257
+ `redirect_uri=https://auth.canopy.website/auth/callback`.
258
+
259
+ ### 8. Worker secrets (script)
260
+
261
+ ```bash
262
+ cd infra
263
+ set -a && source .env.runtime && set +a # CF_API_TOKEN + AI_GATEWAY_TOKEN
264
+ # OIDC_CLIENT_SECRET: same OneLogin confidential-client secret as before if
265
+ # the app is unchanged. SESSION_SECRET: blank prompt = generate a fresh one.
266
+ OIDC_CLIENT_SECRET="…" CF_API_TOKEN="…" AI_GATEWAY_TOKEN="…" \
267
+ ./scripts/bootstrap-secrets.sh main # or: staging
268
+ ```
269
+
270
+ Sets secrets on both workers; **`SESSION_SECRET` is written identically to api
271
+ and gateway** — the script handles that. Secrets take effect immediately (no
272
+ redeploy required).
273
+
274
+ ### 9. OneLogin (external, dashboard)
275
+
276
+ Only if the host changed and the apps aren't already pointed at it. OneLogin
277
+ matches redirect URIs exactly (no wildcards):
278
+
279
+ - Gateway confidential client → `https://auth.canopy.website/auth/callback`
280
+ - CLI PKCE client → unchanged (`http://127.0.0.1:8976/callback`, local)
281
+
282
+ If you already configured these for `canopy.website` before the move, skip.
283
+
284
+ ### 10. Log in, grant yourself admin, then deploy the core projects
285
+
286
+ The three core projects are ordinary platform projects that dogfood the product.
287
+ Two of their slugs (`dashboard`, `www`) are in `RESERVED_SLUGS`
288
+ (`apps/api/src/routes/projects.ts`), so **only an admin can create them** —
289
+ hence the ordering here.
290
+
291
+ Point the local CLI at the new platform (and rebuild if you just changed
292
+ defaults):
293
+
294
+ ```bash
295
+ # canopy-dev = this repo's CLI (packages/cli → vp pack), not the npm `canopy`
296
+ export CANOPY_API_URL="https://canopy-api.monks.workers.dev"
297
+ export CANOPY_PROJECT_DOMAIN="canopy.website"
298
+ export CANOPY_PROJECT_PROTOCOL="https"
299
+ ```
300
+
301
+ 1. `canopy-dev login` → browser PKCE against OneLogin; your user row is
302
+ auto-provisioned in the new control DB. Confirm with `canopy-dev whoami`
303
+ (apiUrl should be the new workers.dev host).
304
+ 2. Grant admin (the row only exists after step 1). The first admin on a fresh
305
+ control DB still needs SQL; after that, admins can promote others from the
306
+ dashboard **Users** page:
307
+ ```bash
308
+ pnpm --filter api exec wrangler d1 execute canopy-control --remote \
309
+ --command "UPDATE users SET is_admin = 1 WHERE email = 'you@monks.com'"
310
+ ```
311
+ 3. Register each project remotely, then deploy it. **`--no-scaffold` is
312
+ required** — `create` otherwise writes a starter template into `./<slug>/`
313
+ (and prompts for a template picker), which inside an existing app means a
314
+ junk `apps/dashboard/dashboard/` directory. `--no-scaffold` registers only,
315
+ no files written. The `--slug` must match the `slug` in that directory's
316
+ existing `canopy.json`, since that's what `deploy` publishes to:
317
+
318
+ ```bash
319
+ # Always pass --profile <site> for the destination (main / staging / gcp-pilot).
320
+ # Examples below use main (production dogfood); swap the id when targeting another site.
321
+ cd apps/dashboard
322
+ canopy-dev --profile main create "Canopy Dashboard" --slug dashboard --no-scaffold
323
+ canopy-dev --profile main deploy
324
+
325
+ cd ../www
326
+ canopy-dev --profile main create "Canopy" --slug www --no-scaffold
327
+ canopy-dev --profile main deploy
328
+
329
+ cd ../../examples/kitchen-sink
330
+ canopy-dev --profile main create "Kitchen Sink" --slug kitchen-sink --no-scaffold
331
+ # CF target: capabilities.workers = false — strip worker for this deploy
332
+ cp canopy.json canopy.json.bak
333
+ # remove the "worker" key (jq / editor), then:
334
+ canopy-dev --profile main deploy
335
+ mv canopy.json.bak canopy.json
336
+ ```
337
+
338
+ `canopy.json` already carries the slug, build command, dist dir, and a
339
+ usual `"profile": "main"` pin, so `deploy` builds them — no separate
340
+ `pnpm build` needed. Still pass `--profile` for the intended site so a
341
+ stray active profile cannot send the deploy elsewhere
342
+ (docs/10-cli-sites.md). A kitchen-sink deploy that keeps
343
+ `"worker": "worker/index.ts"` will 501 `not_supported_on_target` on
344
+ Cloudflare; assets-only is enough to exercise `db` / `blob` / `ai.chat`
345
+ through the platform data plane.
346
+
347
+ ### 11. AI Gateway keys + spend rule (script)
348
+
349
+ ```bash
350
+ cd infra # adjust if you're coming from examples/kitchen-sink
351
+ set -a && source .env.runtime && set +a
352
+ # CF_API_TOKEN has AI Gateway Read (use that for the logs check);
353
+ # the script's prompt is about dashboard BYOK / spend-limit clicks.
354
+ export CLOUDFLARE_API_TOKEN="$CF_API_TOKEN"
355
+ CANOPY_ACCOUNT_ID="$(tofu output -raw account_id)" DEFAULT_BUDGET_USD=25 \
356
+ ./scripts/bootstrap-ai-gateway.sh
357
+ ```
358
+
359
+ Verifies the gateway, then walks you through adding BYOK provider keys and the
360
+ per-project spend-limit rule (both dashboard steps — see the script's notes).
361
+ `AI_GATEWAY_TOKEN` alone is **Run**-scoped and cannot read logs; use
362
+ `CF_API_TOKEN` (D1 Write + AI Gateway Read) for the read-back check.
363
+
364
+ The hard daily budget cap is enforced by canopy-api's own `ai_spend` ledger on
365
+ every request; the AI Gateway spend rule is belt-and-braces on top.
366
+
367
+ ### 12. Smoke test
368
+
369
+ Anonymous visits to project hosts **302 to OneLogin** by design (monk SSO;
370
+ same as the old `thanarie.dev` setup). Client-password mode is the only
371
+ anonymous path. Smoke through a real browser session:
372
+
373
+ 1. `https://dashboard.canopy.website` → OneLogin login works; the project list
374
+ shows all three under **Core projects**.
375
+ 2. `https://www.canopy.website` → marketing site + `/changelog` render (after
376
+ SSO).
377
+ 3. `https://kitchen-sink.canopy.website` → exercise `db`, `blob`, and `ai.chat`
378
+ (proves per-project D1 provisioning, R2 prefixes, and the AI Gateway path —
379
+ BYOK must be set first for `ai.chat`).
380
+ 4. Confirm kitchen-sink's LLM spend appears in its dashboard usage view (the
381
+ view reads the platform's own `ai_spend` ledger, written at call time — the
382
+ same rows the daily budget cap enforces).
383
+ 5. `canopy-dev create scratch-…` then `canopy-dev delete <slug> --yes` →
384
+ confirms provisioning and full teardown.
385
+
386
+ CLI sanity (no browser): `curl -sS https://auth.canopy.website/auth/login`
387
+ should 302 to OneLogin; `curl -sS https://canopy-api.<subdomain>.workers.dev/control/v1/docs`
388
+ should 401 `unauthenticated` (API up, auth required).
389
+
390
+ ---
391
+
392
+ ## Teardown of the old account
393
+
394
+ Once the new account is verified and you no longer need the old projects'
395
+ data, delete resources in the old account (or leave it to lapse). Site
396
+ bindings live in `wrangler.main.jsonc` / `wrangler.staging.jsonc`; local
397
+ `wrangler.jsonc` is not a deploy target.