@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,318 @@
1
+ # How static files are served
2
+
3
+ This explains the full path a browser request takes to receive a deployed
4
+ project's `index.html` and its assets (JS/CSS/images/fonts), and reviews that
5
+ path from a **security** and a **performance** angle.
6
+
7
+ Everything a visitor sees is a _deployed_ build stored in R2. On Cloudflare
8
+ there is no origin server running the project's code — the platform serves
9
+ pre-built static files. (On the GCP target, `/api/*` requests dispatch to a
10
+ per-project Cloud Run backend; everything else is still static serving. On
11
+ Cloudflare, per-project workers remain a Phase 2 TODO and the capability is
12
+ gated off.)
13
+
14
+ Asset serving is the hottest **data-plane** path (see
15
+ [`docs/09-plane-split.md`](../09-plane-split.md)): it lives at
16
+ `/data/v1/assets/*`, and the URL carries no project slug — the gateway stamps
17
+ which project the request is for.
18
+
19
+ ## The request path
20
+
21
+ A request to `https://<slug>.canopy.<domain>/some/path` flows through two
22
+ Workers and one R2 bucket:
23
+
24
+ ```
25
+ Browser
26
+ │ GET https://dashboard.canopy.example.com/assets/app-a1b2.js
27
+ ▼
28
+ canopy-gateway (apps/gateway/src/index.ts)
29
+ │ 1. Parse <slug> (and optional pinned version) from the hostname
30
+ │ 2. Enforce auth — OneLogin session cookie, or client-password cookie
31
+ │ 3. Rewrite path → /data/v1/assets/<path> (+ ?v=<n> if pinned),
32
+ │ stamping X-Canopy-Project: <slug> + an internal JWT vouching for it
33
+ ▼ env.API.fetch(...) (service binding, no public hop)
34
+ canopy-api (apps/api/src/routes/assets.ts)
35
+ │ 4. Resolve the project from the stamp + which version is live
36
+ │ 5. Sanitize the asset path, SPA-fallback to index.html
37
+ │ 6. R2 GET deploys/<project-id>/<version>/<path>
38
+ ▼
39
+ R2 bucket "canopy-assets"
40
+ ```
41
+
42
+ ### 1–3. Gateway: hostname, auth, path rewrite
43
+
44
+ [`apps/gateway/src/index.ts`](../../apps/gateway/src/index.ts) is the front door
45
+ for every `*.canopy.<domain>` request.
46
+
47
+ - **Hostname → slug + version.** `parseHost()`
48
+ ([`index.ts`](../../apps/gateway/src/index.ts)) splits the first DNS label.
49
+ A plain `dashboard` resolves to that project's _live_ version; a
50
+ `v12-dot-dashboard` prefix pins version 12 of the **frontend only** — auth,
51
+ client access, and the data plane still resolve to the real project, so an old
52
+ build previews against _current_ data rather than forking it.
53
+ - **Auth before any project bytes.** No asset is served unauthenticated. A
54
+ request must carry either a valid OneLogin session cookie (verified via a
55
+ shared `SESSION_SECRET`) or — on client-enabled projects — a scope-limited
56
+ client-password cookie. Missing both: client-enabled projects get the password
57
+ page ([`client.ts`](../../apps/gateway/src/client.ts)), everything else
58
+ redirects to OneLogin. Collaborators-only projects additionally turn
59
+ non-members away with a members-only page before any dispatch.
60
+ - **Path rewrite + project stamp.** Once authorized, the gateway rewrites the
61
+ request to the data plane and forwards it over a **service binding**
62
+ (`env.API.fetch`), never a public URL
63
+ ([`index.ts`](../../apps/gateway/src/index.ts)). The URL carries no slug —
64
+ the project travels as a header the gateway vouches for:
65
+
66
+ ```ts
67
+ const assetUrl = new URL(request.url);
68
+ assetUrl.pathname = `/data/v1/assets${url.pathname}`;
69
+ if (version !== null) assetUrl.searchParams.set("v", String(version));
70
+ const assetRequest = new Request(assetUrl, request);
71
+ assetRequest.headers.delete("x-canopy-project");
72
+ assetRequest.headers.set("x-canopy-project", slug);
73
+ assetRequest.headers.set("x-canopy-internal", await createInternalJwt(env.SESSION_SECRET));
74
+ return env.API.fetch(assetRequest);
75
+ ```
76
+
77
+ The caller's cookie travels along, so canopy-api re-verifies it independently,
78
+ and canopy-api trusts the `X-Canopy-Project` stamp only when the accompanying
79
+ internal JWT verifies.
80
+
81
+ `/_api/*` requests are handled separately, forwarded to the API before the
82
+ session wall so the SDK gets JSON 401s instead of login HTML. The plane is the
83
+ path: `/_api/control/v1/*` forwards untouched, `/_api/data/v1/*` gets the same
84
+ project stamp as asset requests.
85
+
86
+ ### 4–6. API: version resolution, path sanitizing, R2 read
87
+
88
+ The serving handler is `GET /data/v1/assets/*` in
89
+ [`apps/api/src/routes/assets.ts`](../../apps/api/src/routes/assets.ts).
90
+
91
+ - **Project + status.** Resolves the project from the gateway's vouched stamp
92
+ only (`vouchedProject` — never a query string), via a cached
93
+ `ProjectSnapshot` from the `ProjectDirectory`
94
+ ([`src/directory.ts`](../../apps/api/src/directory.ts)) — the data plane
95
+ never queries the control database directly. Unknown → 404 placeholder;
96
+ paused (`status !== "active"`) → a "paused" placeholder page. Viewability is
97
+ company-wide by default; projects marked **collaborators-only**
98
+ (`members_only`) turn non-members away with a 403 placeholder
99
+ (`checkAppAccess`, [`src/access.ts`](../../apps/api/src/access.ts)).
100
+ - **Which version.** `?v=<n>` pins an exact version (must exist in the
101
+ project's deploy history, else 404 — it never silently falls back to live);
102
+ otherwise the snapshot's `liveAssetVersion` is served, which falls back to
103
+ the max deployed version for legacy rows with a NULL pointer.
104
+ - **Path → R2 key.** The tail after `/assets/` is sanitized (see Security),
105
+ defaulting to `index.html`, and mapped to the R2 key
106
+ `deploys/<project-id>/<version>/<path>`.
107
+ - **SPA fallback + read**: the exact asset is read first, and any miss falls
108
+ back to `index.html`, so client-side routes (`/settings`, `/projects/42`) all
109
+ resolve to the SPA entry point. The fallback `index.html` is served from a
110
+ per-version cache (see [Performance](#performance-review)), so a deep-linked
111
+ route doesn't pay a second R2 read on every request.
112
+
113
+ ### How assets are stored & versioned
114
+
115
+ `canopy deploy` uses a two-step signed upload
116
+ ([`docs/09-plane-split.md`](../09-plane-split.md) D-d): the **control plane**
117
+ authorizes the deploy (`POST /control/v1/projects/:slug/deploys`,
118
+ [`deploys.ts`](../../apps/api/src/routes/deploys.ts)) and issues a short-lived
119
+ signed grant; the CLI PUTs each file to the **data plane**
120
+ (`PUT /data/v1/uploads/:grant`,
121
+ [`data-plane.ts`](../../apps/api/src/routes/data-plane.ts)); a completion call
122
+ (`POST …/deploys/:version/complete`) validates the staged bundle and records
123
+ the version. Bundle bytes never transit the control plane.
124
+
125
+ - Each file is written to R2 at `deploys/<project-id>/<version>/<path>` with its
126
+ `contentType` stored as R2 `httpMetadata` by the upload endpoint.
127
+ - The bucket is `canopy-assets`, bound as `ASSETS`
128
+ ([`apps/api/wrangler.main.jsonc`](../../apps/api/wrangler.main.jsonc)).
129
+ - **Immutable, append-only versioning.** A deploy bumps the version and writes a
130
+ new prefix; the `deployments` row commits at completion _before_
131
+ `live_asset_version` moves, so a half-uploaded deploy is never served (an
132
+ abandoned grant leaves only unreferenced staged bytes). Rollback
133
+ (`POST …/deploys/:version/rollback`,
134
+ [`deploys.ts`](../../apps/api/src/routes/deploys.ts)) just repoints
135
+ `live_asset_version` at an existing version — instant, nothing copied, nothing
136
+ deleted, and any version can be made live again. Old versions keep their bytes
137
+ and their identity, which is what makes `v<n>-dot-` preview hosts work.
138
+ - Project deletion purges the whole `deploys/<project-id>/` prefix (plus blobs
139
+ and snapshots) so no R2 residue remains
140
+ ([`projects.ts`](../../apps/api/src/routes/projects.ts)).
141
+
142
+ The browser SDK itself is served by the platform, not from a deploy bundle:
143
+ `GET /_api/data/v1/sdk.js` returns an inlined bundle
144
+ ([`data-plane.ts`](../../apps/api/src/routes/data-plane.ts)) so scaffolded
145
+ projects need no `npm install`.
146
+
147
+ ---
148
+
149
+ ## Security review
150
+
151
+ ### What's solid
152
+
153
+ - **Nothing is public.** Every asset request passes the gateway auth wall
154
+ (OneLogin session or client-password cookie) before any project bytes are
155
+ read, and the gateway→API hop is a private service binding, not an internet
156
+ URL. This is the north-star "never public URLs" guarantee.
157
+ - **Path-traversal defense.** `safeAssetPath()`
158
+ ([`assets.ts`](../../apps/api/src/routes/assets.ts)) rejects on both
159
+ ingest (the upload endpoint) and serve: strips a leading `./` or `/`, then
160
+ rejects empty paths, paths over 512 chars, and any path containing `\`, `//`,
161
+ `\0`, or a `.`/`..`/empty segment. A malicious key can't escape the
162
+ `deploys/<project-id>/<version>/` prefix, and one project can never read
163
+ another's bytes because the prefix is keyed by the resolved project id.
164
+ - **Tenant isolation by key prefix and by stamp.** The R2 key always embeds the
165
+ resolved `project.id`, so cross-project reads are structurally impossible even
166
+ if a path slipped through. Serving resolves its project from the gateway's
167
+ vouched `X-Canopy-Project` stamp alone — never a query parameter — so a
168
+ crafted query string can't render one project's pages under another's host.
169
+ - **Content-Type is fixed at deploy time**, from an allowlist keyed by extension
170
+ (`contentTypeFor()`, [`assets.ts`](../../apps/api/src/routes/assets.ts)),
171
+ and stored as R2 metadata by the upload endpoint. Serving echoes that stored
172
+ type; unknown extensions become `application/octet-stream` (download, not
173
+ execute). The type is never derived from request input at serve time.
174
+ - **Deploy bundle limits.** Max 500 files and 25 MB total
175
+ ([`storage-keys.ts`](../../apps/api/src/storage-keys.ts)), enforced per file
176
+ at upload and re-checked over the staged inventory at completion; a bundle
177
+ must contain `index.html` or completion is rejected — bounds the blast radius
178
+ of a bad upload. Upload grants are HMAC-signed, project- and version-bound,
179
+ and expire after 15 minutes ([`upload-grant.ts`](../../apps/api/src/upload-grant.ts)).
180
+ - **Password page escapes untrusted input.** `escapeHtml()` and the same-site
181
+ `safeLocalPath()`/`safeReturnTo()` guards prevent reflected-XSS and open-redirect
182
+ via the `returnTo` parameter
183
+ ([`client.ts`](../../apps/gateway/src/client.ts),
184
+ [`index.ts`](../../apps/gateway/src/index.ts)).
185
+ - **Version pinning can't leak data.** A `v<n>-dot-` host pins only _which
186
+ frontend build_ is served; auth, client-access, and all `/_api` data-plane
187
+ calls resolve to the real project (the stamp names the real slug),
188
+ so an old build cannot be used to read a different project's data.
189
+
190
+ ### Gaps / things to weigh
191
+
192
+ These are observations, not known incidents — worth a conscious decision:
193
+
194
+ 1. **No hardening response headers.** Served assets carry `content-type`, `etag`,
195
+ and `cache-control` only ([`assets.ts`](../../apps/api/src/routes/assets.ts)).
196
+ There is **no** `X-Content-Type-Options: nosniff`, no `Content-Security-Policy`,
197
+ no `X-Frame-Options`/`frame-ancestors`, no `Referrer-Policy`, and no HSTS. For
198
+ an app platform serving arbitrary user HTML/JS on shared `*.canopy.<domain>`
199
+ subdomains, the most impactful additions are:
200
+ - `X-Content-Type-Options: nosniff` — cheap, prevents MIME-sniffing of e.g. a
201
+ user-uploaded file into script.
202
+ - `frame-ancestors`/`X-Frame-Options` — clickjacking, and stops one canopy
203
+ subdomain from framing another.
204
+ - A default `Content-Security-Policy` (even a permissive one) as a baseline.
205
+ Note projects run against a shared cookie domain, so cross-subdomain
206
+ concerns (cookie scoping, framing) matter more here than on an isolated
207
+ site.
208
+ 2. **`text/html` is served for deployed HTML with the session cookie present.**
209
+ That's by design (auth-gated apps), but it means any XSS in a deployed project
210
+ runs on a `*.canopy.<domain>` origin that shares the session-cookie domain.
211
+ The session cookie's flags (HttpOnly/SameSite/Secure) in
212
+ [`session.ts`](../../apps/gateway/src/session.ts) are the mitigation to keep an
213
+ eye on; a per-project origin isolation (Phase 2 dev workers) would be the
214
+ stronger fix.
215
+ 3. **SPA fallback returns `index.html` with 200 for unknown paths**, including
216
+ things that look like missing assets (`/assets/typo.js` → HTML). That's normal
217
+ SPA behavior but can mask 404s and, combined with #1, is another reason
218
+ `nosniff` matters (an HTML body served where a script was expected).
219
+ 4. **Serving is company-wide readable by default.** Any authenticated monk can
220
+ view any active project unless it opts into collaborators-only
221
+ (`members_only`, enforced in [`assets.ts`](../../apps/api/src/routes/assets.ts)
222
+ via `checkAppAccess`). The default is an intentional product decision (see
223
+ docs/02-decisions.md), not a bug — but it means "authenticated" ≠
224
+ "authorized for this specific project" on the read path unless the project
225
+ tightens it.
226
+
227
+ ---
228
+
229
+ ## Performance review
230
+
231
+ ### What helps
232
+
233
+ - **Split caching policy by type** ([`assets.ts`](../../apps/api/src/routes/assets.ts)):
234
+
235
+ ```ts
236
+ "cache-control": type.startsWith("text/html")
237
+ ? "no-cache" // HTML always revalidates → new deploys show up
238
+ : "private, max-age=3600", // hashed subresources cache for 1h
239
+ ```
240
+
241
+ HTML revalidates so a fresh deploy is picked up immediately; hashed JS/CSS/font
242
+ assets cache in the browser for an hour. This is the right shape for a
243
+ content-hashed SPA build.
244
+
245
+ - **ETags** (`"v<version>-<path>"`, [`assets.ts`](../../apps/api/src/routes/assets.ts))
246
+ give a stable validator per version+path, so revalidation of `no-cache` HTML
247
+ can come back as a cheap `304`. Because the etag encodes the version, a rollback
248
+ or new deploy changes the validator and busts caches correctly.
249
+ - **Instant deploys and rollbacks.** Serving reads a pointer
250
+ (`live_asset_version`); deploy/rollback move the pointer without copying bytes,
251
+ so there's no propagation delay and no rebuild.
252
+ - **Service-binding hop.** Gateway→API is an in-Cloudflare service binding, not a
253
+ public fetch, so there's no extra internet round-trip or TLS handshake between
254
+ the two Workers.
255
+
256
+ ### Bottlenecks / opportunities
257
+
258
+ 1. **`max-age=3600` is modest for hashed assets.** Content-hashed filenames (the
259
+ Vite default) can safely use `public, max-age=31536000, immutable`. The current
260
+ value is `private` (see #2) and only an hour, so returning visitors refetch
261
+ more than necessary. Consider keying on filename hash pattern or a deploy-time
262
+ flag to mark truly immutable assets.
263
+ 2. **`private` prevents any shared/edge caching.** Because assets are auth-gated
264
+ and served `private`, they can't be cached by Cloudflare's CDN or any shared
265
+ proxy — every cache miss goes Worker→R2. For an auth-walled platform that's a
266
+ deliberate trade (privacy over edge-cache hit rate), but it means R2 latency is
267
+ on the critical path for every uncached request. If per-project caching is ever
268
+ wanted, the Cache API keyed by (project, version, path) inside the Worker would
269
+ let you cache without making anything public.
270
+ 3. **No compression / `Content-Encoding`.** The handler streams R2 bytes as-is
271
+ with no gzip/brotli negotiation and ignores `Accept-Encoding`. Cloudflare may
272
+ apply compression at the edge for eligible content types, but nothing is stored
273
+ pre-compressed and the Worker doesn't negotiate it. For large JS bundles,
274
+ storing brotli-precompressed variants (or relying on and verifying edge
275
+ compression) is worth measuring.
276
+ 4. **Range requests / streaming media.** The handler returns the full R2 object
277
+ body and doesn't honor `Range`. Fine for a bundled SPA; if projects ever serve
278
+ large video/audio, add `Range` support (R2 supports ranged `get`).
279
+
280
+ ### Addressed — in-worker caches
281
+
282
+ Two former bottlenecks are now handled by best-effort, per-isolate caches
283
+ (a performance layer only — never a source of truth; a cold isolate just reads
284
+ D1/R2):
285
+
286
+ - **SPA fallback no longer double-reads R2.** The serve path reads the exact
287
+ asset once; on a miss it falls back to `index.html` served from a per-`(project,
288
+ version)` cache ([`asset-cache.ts`](../../apps/api/src/asset-cache.ts)). Because a
289
+ deployed version's bytes are immutable, that entry is cached indefinitely
290
+ (count-bounded), so the second R2 read is paid **once per version**, not once
291
+ per deep-linked route. Verified by a test that serves three client routes and
292
+ asserts a single `index.html` read.
293
+ - **Project state is cached per slug** as a `ProjectSnapshot` in the
294
+ `ProjectDirectory` ([`src/directory.ts`](../../apps/api/src/directory.ts) —
295
+ the generalization of the old metadata cache), sparing the hot path its
296
+ control-DB queries. Since this data _does_ change, deploy, rollback, and
297
+ delete call `invalidateSnapshot()` so a write is reflected immediately on the
298
+ same isolate; a short TTL (5 s) is only a backstop for a write that lands on
299
+ a different isolate than the next read.
300
+
301
+ ---
302
+
303
+ ## Quick reference
304
+
305
+ | Concern | Where | Notes |
306
+ | --------------------------- | ------------------------------------------------------------------------------ | ---------------------------------------------------------------------- |
307
+ | Front door / auth / routing | [`apps/gateway/src/index.ts`](../../apps/gateway/src/index.ts) | Hostname→slug, auth wall, `/data/v1/assets` rewrite + project stamp |
308
+ | Client-password page | [`apps/gateway/src/client.ts`](../../apps/gateway/src/client.ts) | HTML-escaped, same-site returnTo |
309
+ | Asset serve handler | [`apps/api/src/routes/assets.ts`](../../apps/api/src/routes/assets.ts) | Version resolve, path sanitize, SPA fallback, headers |
310
+ | Project snapshots | [`apps/api/src/directory.ts`](../../apps/api/src/directory.ts) | `ProjectDirectory` cache (5 s TTL), invalidated on write |
311
+ | index.html cache | [`apps/api/src/asset-cache.ts`](../../apps/api/src/asset-cache.ts) | Per-version, immutable bytes, count-bounded |
312
+ | Path sanitizing | [`assets.ts`](../../apps/api/src/routes/assets.ts) | `safeAssetPath()` — traversal defense, shared with the upload endpoint |
313
+ | Content-Type allowlist | [`assets.ts`](../../apps/api/src/routes/assets.ts) | Fixed at deploy, stored in R2 metadata |
314
+ | Deploy authorize / complete | [`apps/api/src/routes/deploys.ts`](../../apps/api/src/routes/deploys.ts) | Signed grant, 500 files / 25 MB, pointer moves last |
315
+ | Upload bytes | [`apps/api/src/routes/data-plane.ts`](../../apps/api/src/routes/data-plane.ts) | `PUT /data/v1/uploads/:grant`, grant is the credential |
316
+ | Rollback | [`deploys.ts`](../../apps/api/src/routes/deploys.ts) | Repoints `live_asset_version` |
317
+ | R2 bucket binding | [`apps/api/wrangler.main.jsonc`](../../apps/api/wrangler.main.jsonc) | `ASSETS` → `canopy-assets` |
318
+ | SDK serving | [`apps/api/src/routes/data-plane.ts`](../../apps/api/src/routes/data-plane.ts) | `/_api/data/v1/sdk.js`, `public, max-age=300` |
@@ -47,6 +47,20 @@ Run it on your own laptop.
47
47
  The `CANOPY_DEPLOY_TOKEN` was revoked or is wrong. Create a new one with
48
48
  `canopy token create` (or in the dashboard) and update your CI secret.
49
49
 
50
+ ## `not_supported_on_target` (501)
51
+
52
+ Some features are platform-target-specific: on the Cloudflare target,
53
+ per-project worker backends, dashboard schedules, and secret injection are not
54
+ available and answer this error (the dashboard hides those panels). It's not a
55
+ failure of your project — the feature doesn't exist on the target your platform
56
+ runs on.
57
+
58
+ ## `ai_budget_exceeded` (429)
59
+
60
+ The project hit its daily AI budget cap. The cap is enforced before every AI
61
+ call; it resets the next day, or a maintainer can raise the project's budget in
62
+ the dashboard.
63
+
50
64
  ## Docs are stale
51
65
 
52
66
  `canopy docs` caches the platform's docs. Force a refresh:
@@ -1,4 +1,5 @@
1
- import { l as readConfig, n as reportErrors } from "./ui-D1DXoDN1.mjs";
1
+ import { n as siteConfig } from "./site-BJjoePQS.mjs";
2
+ import { n as reportErrors } from "./ui-DThlyHgo.mjs";
2
3
  import { defineCommand } from "citty";
3
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
4
5
  import { homedir } from "node:os";
@@ -7,7 +8,7 @@ import { fileURLToPath } from "node:url";
7
8
  //#region src/docs.ts
8
9
  /**
9
10
  * canopy docs — user/agent-facing guides, served live by the platform
10
- * (GET /v1/docs) so they never go stale, with a local cache and a bundled
11
+ * (GET /control/v1/docs) so they never go stale, with a local cache and a bundled
11
12
  * snapshot as offline fallbacks.
12
13
  *
13
14
  * The platform requires a login for docs (internal platform), so fetches carry
@@ -45,7 +46,7 @@ function parseFrontmatter(raw) {
45
46
  }
46
47
  /** Bearer header from the deploy token (CI) or the CLI login, if either exists. */
47
48
  function authHeaders() {
48
- const token = process.env["CANOPY_DEPLOY_TOKEN"] || readConfig().token;
49
+ const token = process.env["CANOPY_DEPLOY_TOKEN"] || siteConfig().token;
49
50
  return token ? { authorization: `Bearer ${token}` } : {};
50
51
  }
51
52
  async function fetchRes(url) {
@@ -114,9 +115,9 @@ function bodyFromFallback(slug) {
114
115
  if (file && existsSync(file)) return parseFrontmatter(readFileSync(file, "utf8")).body;
115
116
  }
116
117
  async function listTopics(refresh) {
117
- const { apiUrl } = readConfig();
118
+ const { apiUrl } = siteConfig();
118
119
  if (refresh || !cacheFresh()) {
119
- const data = await fetchJson(`${apiUrl}/v1/docs`);
120
+ const data = await fetchJson(`${apiUrl}/control/v1/docs`);
120
121
  if (data?.topics) {
121
122
  writeCache("index.json", JSON.stringify({
122
123
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -128,9 +129,9 @@ async function listTopics(refresh) {
128
129
  return topicsFromFallback();
129
130
  }
130
131
  async function readTopic(slug, refresh) {
131
- const { apiUrl } = readConfig();
132
+ const { apiUrl } = siteConfig();
132
133
  if (refresh || !existsSync(join(cacheDir, `${slug}.md`))) {
133
- const body = await fetchText(`${apiUrl}/v1/docs/${encodeURIComponent(slug)}`);
134
+ const body = await fetchText(`${apiUrl}/control/v1/docs/${encodeURIComponent(slug)}`);
134
135
  if (body !== void 0) {
135
136
  writeCache(`${slug}.md`, body);
136
137
  return body;
package/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { t as version } from "./package-Bnu23N0A.mjs";
3
- import { i as runRefresh, r as notifyOfUpdate } from "./version-check-CoWgQs2h.mjs";
2
+ import { t as version } from "./package-CHoZKzaw.mjs";
3
+ import { r as siteHeaderLine, t as setInvocationProfile } from "./site-BJjoePQS.mjs";
4
+ import { i as runRefresh, r as notifyOfUpdate } from "./version-check-Cuh6HaLX.mjs";
4
5
  import { defineCommand, runMain } from "citty";
5
6
  //#region src/index.ts
6
7
  /**
@@ -15,12 +16,43 @@ import { defineCommand, runMain } from "citty";
15
16
  * token create|list|revoke deploy tokens for CI [phase 2]
16
17
  * db pull|snapshot|restore [phase 2]
17
18
  * logs tail the project's worker logs [phase 2]
19
+ * usage AI spend + storage for a project [phase 1]
18
20
  * open open the deployed project / dashboard [phase 1]
19
21
  */
20
22
  if (process.env["CANOPY_UPDATE_REFRESH"] === "1") {
21
23
  await runRefresh();
22
24
  process.exit(0);
23
25
  }
26
+ /**
27
+ * Global `--profile <id>` / `--profile=<id>` (docs/10-cli-sites.md).
28
+ * `login` declares its own `--profile` arg — leave it on argv for citty.
29
+ */
30
+ function takeGlobalProfile(argv) {
31
+ if (argv[2] === "login") {
32
+ const i = argv.indexOf("--profile");
33
+ if (i !== -1 && argv[i + 1]) return argv[i + 1];
34
+ return argv.find((a) => a.startsWith("--profile="))?.slice(10);
35
+ }
36
+ const out = [];
37
+ let profile;
38
+ for (let i = 0; i < argv.length; i++) {
39
+ const arg = argv[i];
40
+ if (arg === "--profile" && argv[i + 1]) {
41
+ profile = argv[++i];
42
+ continue;
43
+ }
44
+ if (arg.startsWith("--profile=")) {
45
+ profile = arg.slice(10);
46
+ continue;
47
+ }
48
+ out.push(arg);
49
+ }
50
+ if (profile !== void 0) process.argv = out;
51
+ return profile;
52
+ }
53
+ const profileFlag = takeGlobalProfile(process.argv);
54
+ if (profileFlag) setInvocationProfile(profileFlag);
55
+ else if (process.env["CANOPY_PROFILE"]) setInvocationProfile(process.env["CANOPY_PROFILE"]);
24
56
  /** A roadmap command that has not landed yet (docs/04-roadmap.md). */
25
57
  function planned(name, description, phase) {
26
58
  return defineCommand({
@@ -41,27 +73,29 @@ const main = defineCommand({
41
73
  description: "canopy — create and deploy vibecoded projects"
42
74
  },
43
75
  subCommands: {
44
- login: () => import("./login-DdNc9OjU.mjs").then((m) => m.loginCommand),
45
- whoami: () => import("./login-DdNc9OjU.mjs").then((m) => m.whoamiCommand),
46
- logout: () => import("./login-DdNc9OjU.mjs").then((m) => m.logoutCommand),
47
- create: () => import("./projects-CK4DNxgZ.mjs").then((m) => m.createCommand),
48
- init: () => import("./projects-CK4DNxgZ.mjs").then((m) => m.initCommand),
49
- projects: () => import("./projects-CK4DNxgZ.mjs").then((m) => m.projectsCommand),
50
- delete: () => import("./projects-CK4DNxgZ.mjs").then((m) => m.deleteCommand),
51
- dev: () => import("./dev-BYv6BmcI.mjs").then((m) => m.devCommand),
52
- deploy: () => import("./deploy-B064KXtr.mjs").then((m) => m.deployCommand),
53
- rollback: () => import("./deploy-B064KXtr.mjs").then((m) => m.rollbackCommand),
54
- download: () => import("./deploy-B064KXtr.mjs").then((m) => m.downloadCommand),
55
- token: () => import("./token-yuwQTQB8.mjs").then((m) => m.tokenCommand),
56
- db: () => import("./db-PpaU3V-B.mjs").then((m) => m.dbCommand),
57
- open: () => import("./open-CBuZBJlv.mjs").then((m) => m.openCommand),
58
- docs: () => import("./docs-UYzmzpz-.mjs").then((m) => m.docsCommand),
59
- skill: () => import("./skill-h7QlCMhc.mjs").then((m) => m.skillCommand),
60
- upgrade: () => import("./upgrade-sXoqYLRu.mjs").then((m) => m.upgradeCommand),
76
+ login: () => import("./login-BO6abf40.mjs").then((m) => m.loginCommand),
77
+ whoami: () => import("./login-BO6abf40.mjs").then((m) => m.whoamiCommand),
78
+ logout: () => import("./login-BO6abf40.mjs").then((m) => m.logoutCommand),
79
+ create: () => import("./projects-ijfe7Ffx.mjs").then((m) => m.createCommand),
80
+ init: () => import("./projects-ijfe7Ffx.mjs").then((m) => m.initCommand),
81
+ projects: () => import("./projects-ijfe7Ffx.mjs").then((m) => m.projectsCommand),
82
+ delete: () => import("./projects-ijfe7Ffx.mjs").then((m) => m.deleteCommand),
83
+ dev: () => import("./dev-BJ5xEwqW.mjs").then((m) => m.devCommand),
84
+ deploy: () => import("./deploy-alC2--pZ.mjs").then((m) => m.deployCommand),
85
+ rollback: () => import("./deploy-alC2--pZ.mjs").then((m) => m.rollbackCommand),
86
+ download: () => import("./deploy-alC2--pZ.mjs").then((m) => m.downloadCommand),
87
+ token: () => import("./token-BeF9zczg.mjs").then((m) => m.tokenCommand),
88
+ db: () => import("./db-BW1ZolWn.mjs").then((m) => m.dbCommand),
89
+ usage: () => import("./usage-DQpH7A3X.mjs").then((m) => m.usageCommand),
90
+ open: () => import("./open-DnlYDqrU.mjs").then((m) => m.openCommand),
91
+ docs: () => import("./docs-DvSPpSIR.mjs").then((m) => m.docsCommand),
92
+ skill: () => import("./skill-CW0QSbAJ.mjs").then((m) => m.skillCommand),
93
+ upgrade: () => import("./upgrade-CGz1yzsW.mjs").then((m) => m.upgradeCommand),
61
94
  logs: planned("logs", "Tail worker logs", "2")
62
95
  }
63
96
  });
64
97
  if (process.argv[2] !== "upgrade") notifyOfUpdate(version);
98
+ if (process.argv[2] && process.argv[2] !== "--help" && process.argv[2] !== "-h") console.error(siteHeaderLine(version));
65
99
  await runMain(main);
66
100
  //#endregion
67
101
  export {};