@mediamonks/canopy-cli 0.7.11 → 0.7.13

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 (29) hide show
  1. package/dist/{db-8Etjmzxh.mjs → db-DKnJQPm5.mjs} +2 -2
  2. package/dist/{deploy-BCtEY1is.mjs → deploy-CnxJ4TE6.mjs} +10 -3
  3. package/dist/dev-BqLvttKc.mjs +2 -0
  4. package/dist/{dev-DtoZrexp.mjs → dev-CyJT6EfQ.mjs} +7 -6
  5. package/dist/{dev-worker-BZgtEM9V.mjs → dev-worker-IWnHvQ1f.mjs} +42 -7
  6. package/dist/docs/index.json +5 -0
  7. package/dist/docs/platform-google.md +213 -0
  8. package/dist/docs/static-serving.md +14 -9
  9. package/dist/{docs-BV9qhLJx.mjs → docs-BBMVxVta.mjs} +2 -2
  10. package/dist/{egress-CN_axsAN.mjs → egress-BFSwa8Uu.mjs} +1 -1
  11. package/dist/{feedback-Cb8ccQ_R.mjs → feedback-BMX_d4KZ.mjs} +2 -2
  12. package/dist/index.mjs +25 -24
  13. package/dist/{local-C5AhA4GM.mjs → local-BQxsp-ZQ.mjs} +7 -470
  14. package/dist/{login-D7dI8D9N.mjs → login-BDTO3nVw.mjs} +1 -1
  15. package/dist/{logs-DNiZquki.mjs → logs-C53y2eAS.mjs} +1 -1
  16. package/dist/{migrations-C-5x6SYf.mjs → migrations-C6EcKD6a.mjs} +1 -1
  17. package/dist/{open-DRPp4QJg.mjs → open-C5x3tX6I.mjs} +1 -1
  18. package/dist/{package-BBpw-n7k.mjs → package-vJXq4RNN.mjs} +1 -1
  19. package/dist/{projects-C6JQjVqg.mjs → projects-MMbv7MmK.mjs} +1 -1
  20. package/dist/schedule-Cq9K0rnE.mjs +106 -0
  21. package/dist/{skill-DJHlTEhS.mjs → skill-Zq9aIPKf.mjs} +2 -2
  22. package/dist/src-Bu4Ze32I.mjs +470 -0
  23. package/dist/{staging-Bw1D_J8M.mjs → staging-D8G4amjO.mjs} +1 -1
  24. package/dist/{token-D7zrIF8G.mjs → token-BUxgRJVA.mjs} +1 -1
  25. package/dist/{ui-2mrJcjYC.mjs → ui-B1Px5y7q.mjs} +17 -0
  26. package/dist/{upgrade-B86Ao6T5.mjs → upgrade-ES8u8_Pn.mjs} +2 -2
  27. package/dist/{usage-CX1Izj_l.mjs → usage-DjNngyvy.mjs} +1 -1
  28. package/package.json +3 -3
  29. package/dist/dev-BYxSHUH6.mjs +0 -2
@@ -1,6 +1,6 @@
1
1
  import { c as resolveProject, o as projectFlagArgs } from "./site-BUlQR117.mjs";
2
- import { i as unwrap, n as reportErrors, o as apiDataRequest, r as requireTty, u as withContext } from "./ui-2mrJcjYC.mjs";
3
- import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-C-5x6SYf.mjs";
2
+ import { i as unwrap, n as reportErrors, o as apiDataRequest, r as requireTty, u as withContext } from "./ui-B1Px5y7q.mjs";
3
+ import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-C6EcKD6a.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  import { readFileSync, writeFileSync } from "node:fs";
6
6
  import * as p from "@clack/prompts";
@@ -1,6 +1,6 @@
1
1
  import { a as projectDirArgs, c as resolveProject, l as stagingUrl, n as siteConfig, o as projectFlagArgs, s as projectUrl, u as tryReadCanopyJson } from "./site-BUlQR117.mjs";
2
- import { c as apiRequest, i as unwrap, n as reportErrors, o as apiDataRequest, s as apiDownload, u as withContext } from "./ui-2mrJcjYC.mjs";
3
- import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-C-5x6SYf.mjs";
2
+ import { c as apiRequest, i as unwrap, n as reportErrors, o as apiDataRequest, s as apiDownload, u as withContext } from "./ui-B1Px5y7q.mjs";
3
+ import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-C6EcKD6a.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  import { mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
6
6
  import { tmpdir } from "node:os";
@@ -113,6 +113,12 @@ const deployCommand = defineCommand({
113
113
  type: "boolean",
114
114
  default: false,
115
115
  description: "Deploy to the project's staging channel (created on first use, database cloned from live)"
116
+ },
117
+ worker: {
118
+ type: "boolean",
119
+ default: true,
120
+ description: "Include the backend worker from canopy.json",
121
+ negativeDescription: "Deploy the frontend only — for sites whose target hosts no per-project backends"
116
122
  }
117
123
  },
118
124
  run: reportErrors("deploy", async ({ args }) => {
@@ -145,7 +151,8 @@ const deployCommand = defineCommand({
145
151
  });
146
152
  }
147
153
  let workerBytes = null;
148
- if (config.worker) {
154
+ if (config.worker && !args.worker) p.log.warn(`Skipping the backend (${config.worker}) — deploying the frontend only.`);
155
+ if (config.worker && args.worker) {
149
156
  const spinWorker = p.spinner();
150
157
  spinWorker.start(`Bundling worker ${config.worker}`);
151
158
  try {
@@ -0,0 +1,2 @@
1
+ import { t as devCommand } from "./dev-CyJT6EfQ.mjs";
2
+ export { devCommand };
@@ -1,6 +1,6 @@
1
1
  import { a as projectDirArgs, c as resolveProject, i as projectDialect, n as siteConfig, u as tryReadCanopyJson } from "./site-BUlQR117.mjs";
2
- import { a as ApiError, c as apiRequest, n as reportErrors, u as withContext } from "./ui-2mrJcjYC.mjs";
3
- import { r as readMigrationFiles } from "./migrations-C-5x6SYf.mjs";
2
+ import { a as ApiError, c as apiRequest, n as reportErrors, u as withContext } from "./ui-B1Px5y7q.mjs";
3
+ import { r as readMigrationFiles } from "./migrations-C6EcKD6a.mjs";
4
4
  import { createRequire } from "node:module";
5
5
  import { defineCommand } from "citty";
6
6
  import { existsSync } from "node:fs";
@@ -37,7 +37,7 @@ async function workerPlugin(flags, slug) {
37
37
  console.log(`note: canopy.json points "worker" at ${entry}, which does not exist — skipping.`);
38
38
  return;
39
39
  }
40
- const { missingSecretNames, readDevVars, workerDevPlugin } = await import("./dev-worker-BZgtEM9V.mjs");
40
+ const { missingSecretNames, readDevVars, workerDevPlugin } = await import("./dev-worker-IWnHvQ1f.mjs");
41
41
  const secrets = readDevVars(flags.dir);
42
42
  const email = siteConfig().email;
43
43
  const plugin = workerDevPlugin({
@@ -165,7 +165,7 @@ async function devRemote(flags, slug) {
165
165
  async function devLocal(flags, slug) {
166
166
  let dump;
167
167
  if (flags.pull) {
168
- const { fetchProjectDump } = await import("./local-C5AhA4GM.mjs");
168
+ const { fetchProjectDump } = await import("./local-BQxsp-ZQ.mjs");
169
169
  console.log(`Pulling a fresh export of ${slug}${flags.staging ? " [staging]" : ""}…`);
170
170
  dump = await fetchProjectDump(slug, flags.staging ? "staging" : "live");
171
171
  }
@@ -180,7 +180,7 @@ async function devLocal(flags, slug) {
180
180
  const reason = err instanceof ApiError ? err.explain().why : String(err);
181
181
  console.log(`Note: ai/services will be unavailable — could not reach the platform (${reason}).`);
182
182
  }
183
- const { createSandbox } = await import("./local-C5AhA4GM.mjs");
183
+ const { createSandbox } = await import("./local-BQxsp-ZQ.mjs");
184
184
  const sandbox = await createSandbox({
185
185
  slug,
186
186
  persistDir: join(flags.dir, ".canopy", "local"),
@@ -233,7 +233,8 @@ async function devLocal(flags, slug) {
233
233
  const server = await startVite(flags, { proxy: {
234
234
  ...remoteEntry && {
235
235
  "/_api/data/v1/ai": remoteEntry,
236
- "/_api/data/v1/services": remoteEntry
236
+ "/_api/data/v1/services": remoteEntry,
237
+ "/_api/data/v1/google": remoteEntry
237
238
  },
238
239
  "/_api": {
239
240
  target: `http://127.0.0.1:${apiAddress.port}`,
@@ -1,5 +1,43 @@
1
+ import "./src-Bu4Ze32I.mjs";
1
2
  import { readFileSync } from "node:fs";
2
3
  import { relative, resolve, sep } from "node:path";
4
+ //#region ../data-plane/src/worker-request.ts
5
+ /**
6
+ * How a project's backend addresses the platform — the one definition.
7
+ *
8
+ * Three hosts run project workers and each builds this request itself: the
9
+ * Workers-for-Platforms shim (apps/api/src/platform/wfp-shim.ts), the Node
10
+ * runtime (apps/project-runtime/src/canopy-fetch.ts), and `canopy dev`
11
+ * (packages/cli/src/dev-worker.ts). They had three copies of the header
12
+ * shaping, and the two that hold a worker token had two copies of the
13
+ * credential rule — which is exactly how the Google project grant came to
14
+ * work on one target and not the other.
15
+ *
16
+ * The shim cannot import this: it ships as a standalone module uploaded
17
+ * beside the user bundle, so it may only import `./bundle.mjs`. It keeps a
18
+ * transcription, and `wfp-shim.test.ts` runs it against the table in
19
+ * `worker-request.cases.ts` alongside this implementation, so the two cannot
20
+ * drift silently.
21
+ */
22
+ /** Gateway-stamped identity, for the worker's own `env.USER` — never relayed. */
23
+ const IDENTITY_STAMP_HEADERS = [
24
+ "x-canopy-user-sub",
25
+ "x-canopy-user-email",
26
+ "x-canopy-user-name"
27
+ ];
28
+ /**
29
+ * Strip what must never travel upstream and stamp who is calling. Common to
30
+ * every host; says nothing about credentials.
31
+ */
32
+ function stampWorkerHeaders(headers, ctx) {
33
+ headers.delete("host");
34
+ for (const stamp of IDENTITY_STAMP_HEADERS) headers.delete(stamp);
35
+ headers.set("x-canopy-project", ctx.projectSlug);
36
+ if (ctx.channel === "staging") headers.set("x-canopy-channel", "staging");
37
+ else headers.delete("x-canopy-channel");
38
+ return headers;
39
+ }
40
+ //#endregion
3
41
  //#region src/dev-worker.ts
4
42
  /**
5
43
  * Paths that belong to the worker rather than the SPA — the gateway's rule
@@ -79,15 +117,12 @@ function rewriteCanopyUrl(raw, base) {
79
117
  function createCanopyFetch(apiBase, slug, channel, fetchImpl = fetch) {
80
118
  return async (input, init) => {
81
119
  const url = rewriteCanopyUrl(typeof input === "string" ? input : input instanceof URL ? input.href : input.url, apiBase());
82
- const headers = new Headers(input instanceof Request ? input.headers : init?.headers);
83
- headers.delete("host");
84
- headers.delete("x-canopy-user-sub");
85
- headers.delete("x-canopy-user-email");
86
- headers.delete("x-canopy-user-name");
120
+ const headers = stampWorkerHeaders(new Headers(input instanceof Request ? input.headers : init?.headers), {
121
+ projectSlug: slug,
122
+ channel
123
+ });
87
124
  headers.delete("authorization");
88
125
  headers.delete("cookie");
89
- headers.set("x-canopy-project", slug);
90
- if (channel === "staging") headers.set("x-canopy-channel", "staging");
91
126
  const method = (input instanceof Request ? input.method : init?.method) ?? "GET";
92
127
  const body = input instanceof Request ? input.body : init?.body;
93
128
  const outbound = {
@@ -124,6 +124,11 @@
124
124
  "title": "AI access & usage",
125
125
  "summary": "Projects talk to AI models through the platform — no API keys to manage, a hard daily budget per project, and full visibility of spend."
126
126
  },
127
+ {
128
+ "slug": "platform-google",
129
+ "title": "Google",
130
+ "summary": "Let your project work with Google data — Sheets, Docs, Calendar, Drive, Gmail — without handling any OAuth yourself."
131
+ },
127
132
  {
128
133
  "slug": "platform-members-and-access",
129
134
  "title": "Members & access",
@@ -0,0 +1,213 @@
1
+ ---
2
+ title: Google
3
+ description: Let your project work with Google data — Sheets, Docs, Calendar, Drive, Gmail — without handling any OAuth yourself.
4
+ ---
5
+
6
+ Your project can work with Google data in two different ways, and the
7
+ difference matters: as **the project's own account**, or as **each visitor's
8
+ own account**.
9
+
10
+ Either way there is no OAuth setup to do. No Cloud project, no consent screen,
11
+ no client ID or secret, no redirect URLs, no tokens to store or refresh. Canopy
12
+ holds all of that.
13
+
14
+ ## The project's own account
15
+
16
+ The owner connects one Google account on the project's **Services** tab and
17
+ picks what it may do. Everything the project sends to Google then happens as
18
+ that account — including scheduled jobs that run when nobody is signed in. A
19
+ shared team account is usually a better choice than a personal one.
20
+
21
+ This account is **only usable from your backend code**:
22
+
23
+ ```ts
24
+ // in your worker
25
+ const doc = await canopy.google.project().fetch("https://docs.googleapis.com/v1/documents/<id>");
26
+ ```
27
+
28
+ Pages cannot use it at all. That is deliberate: it is one person's real
29
+ account, and anything a page can call, any visitor can call from their browser
30
+ console. When your pages need something from it, put a route in your backend
31
+ that returns just that — you decide what to expose.
32
+
33
+ > **Note:** Only the project owner can connect or disconnect this account.
34
+
35
+ ## Each visitor's own account
36
+
37
+ Visitors can connect their own Google account to your project, and your pages
38
+ can then work with _their_ data — their calendar, their documents. They consent
39
+ themselves, and it only ever acts as them.
40
+
41
+ First, on the **Services** tab, tick what your app is allowed to ask visitors
42
+ for. Your app can never request more than that.
43
+
44
+ Then, from your pages:
45
+
46
+ ```ts
47
+ const google = canopy.google.user();
48
+
49
+ // On load: has this visitor connected, and what came back from a connect?
50
+ const result = google.consumeConnectResult(); // toast this, if not null
51
+ const status = await google.status();
52
+
53
+ if (!status.connected) {
54
+ // A full-page trip to Google, then straight back to this page.
55
+ google.connect(["calendar"]);
56
+ }
57
+
58
+ const events = await google.fetch(
59
+ "https://www.googleapis.com/calendar/v3/calendars/primary/events",
60
+ );
61
+ ```
62
+
63
+ `connect()` leaves the page, so put the button somewhere without unsaved work.
64
+ When the visitor comes back, `status()` is the reliable answer to "are they
65
+ connected" — `consumeConnectResult()` just tells you how the trip that _just_
66
+ happened went, so you can show a message.
67
+
68
+ Google asks the visitor for exactly the permissions you passed. Nothing your
69
+ project holds elsewhere carries over into what they see.
70
+
71
+ Your backend can use a visitor's connection too, while handling that visitor's
72
+ request — same `canopy.google.user()`, same account. It cannot in a scheduled
73
+ job, because then nobody is signed in.
74
+
75
+ > **Note:** Visitors need a Monks account to connect. People visiting with a project password cannot connect
76
+ > their own Google account.
77
+
78
+ ## What a connection can reach
79
+
80
+ Permissions are grouped by product — Sheets, Docs, Slides, Calendar, Drive,
81
+ Gmail, Directory, Tasks, Forms, Meet — and most come in a reading and a
82
+ read-and-write form. Pick the narrowest that does the job: a page that shows a
83
+ spreadsheet wants **read spreadsheets**, not read and write.
84
+
85
+ A few are narrower than they first look, and are usually the right answer —
86
+ the full list is below:
87
+
88
+ | Instead of | Consider |
89
+ | ------------------------ | ------------------------------------------------------------------------------------------ |
90
+ | Read and write events | **Availability only** — find free slots without seeing what any meeting is |
91
+ | Read and write events | **Its own calendar** — a separate calendar your project owns, never touching the account's |
92
+ | Files this project makes | **Its own config folder** — a hidden folder nothing else can see |
93
+
94
+ A call to anything that wasn't ticked is refused before it reaches Google, and
95
+ so is a write using a reading-only permission.
96
+
97
+ Drive is deliberately limited to files your project created or that someone
98
+ explicitly opened with it — connecting Drive does not expose a whole Drive.
99
+
100
+ ### Wider permissions
101
+
102
+ A few permissions read broadly across an account — a whole mailbox, meeting
103
+ recordings. Those are switched off until a Canopy admin unlocks them for your
104
+ project, and the picker shows them greyed out with the reason. Ask if you need
105
+ one. Others are listed as _under consideration_: they exist, we haven't opened
106
+ them yet.
107
+
108
+ ## Every permission
109
+
110
+ Grouped the way the picker groups them. Pick the narrowest row that does
111
+ the job — most products offer a reading-only version, and a write using a
112
+ reading-only permission is refused before it reaches Google.
113
+
114
+ **Sheets**
115
+
116
+ | Permission | What it allows |
117
+ | --------------------------- | ----------------------------------------------------------- |
118
+ | Read spreadsheets | Read spreadsheets the account can open. Cannot change them. |
119
+ | Read and write spreadsheets | Read and change spreadsheets the account can open. |
120
+
121
+ **Docs**
122
+
123
+ | Permission | What it allows |
124
+ | ------------------------ | -------------------------------------------------------- |
125
+ | Read documents | Read documents the account can open. Cannot change them. |
126
+ | Read and write documents | Read and change documents the account can open. |
127
+
128
+ **Slides**
129
+
130
+ | Permission | What it allows |
131
+ | ---------------------------- | ------------------------------------------------------------ |
132
+ | Read presentations | Read presentations the account can open. Cannot change them. |
133
+ | Read and write presentations | Build and change presentations the account can open. |
134
+
135
+ **Calendar**
136
+
137
+ | Permission | What it allows |
138
+ | -------------------------------------------- | ----------------------------------------------------------------------------------- |
139
+ | Availability only | See when the account is free or busy, never what the meetings are. |
140
+ | Read events | Read events on the account's calendars. Cannot change them. |
141
+ | Read and write events | Read and manage events on the account's calendars. |
142
+ | Its own calendar | A separate calendar this project creates and owns. Never touches the account's own. |
143
+ | Manage all calendars _(under consideration)_ | Full control of every calendar, including creating and deleting them. |
144
+
145
+ **Drive**
146
+
147
+ | Permission | What it allows |
148
+ | --------------------------------------------- | ----------------------------------------------------------------------------- |
149
+ | Files this project makes | Create files, and reopen the ones this project made or someone picked for it. |
150
+ | Its own config folder | A hidden folder only this project can see. Invisible in the account's Drive. |
151
+ | Recordings and transcripts **(wider access)** | Read the recordings and transcripts Meet saved to Drive. |
152
+ | Read the whole Drive _(under consideration)_ | Read and download every file the account can see. |
153
+ | Read all file names _(under consideration)_ | Names and metadata of every file the account can see, without contents. |
154
+ | Full Drive access _(under consideration)_ | Read, change and delete every file the account can see. |
155
+
156
+ **Gmail**
157
+
158
+ | Permission | What it allows |
159
+ | ---------------------------------------------- | ---------------------------------------------------------------- |
160
+ | Send mail | Send mail as the account. Cannot read the mailbox. |
161
+ | Read mail **(wider access)** | Read the account's mail. Cannot send, change or delete anything. |
162
+ | Read and organise mail _(under consideration)_ | Read, label and archive mail. Cannot permanently delete. |
163
+
164
+ **Directory**
165
+
166
+ | Permission | What it allows |
167
+ | ------------------------------------------------- | -------------------------------------------------------------------- |
168
+ | Look people up | Search the organisation's directory for colleagues. |
169
+ | Read personal contacts | Read the account's own contacts. |
170
+ | Read org users and groups _(under consideration)_ | Admin view of every user and group. Only works for Workspace admins. |
171
+
172
+ **Tasks**
173
+
174
+ | Permission | What it allows |
175
+ | -------------------- | ---------------------------------- |
176
+ | Read tasks | Read the account's task lists. |
177
+ | Read and write tasks | Create, change and complete tasks. |
178
+
179
+ **Forms**
180
+
181
+ | Permission | What it allows |
182
+ | ------------------- | -------------------------------------------------------- |
183
+ | Read form structure | Read a form's questions and settings, not its responses. |
184
+ | Build forms | Create and change forms. |
185
+ | Read responses | Read what people submitted. Often personal information. |
186
+
187
+ **Meet**
188
+
189
+ | Permission | What it allows |
190
+ | --------------------------- | ---------------------------------------------------------- |
191
+ | Meetings this project makes | Create meeting links and read details of the ones it made. |
192
+
193
+ ## When a connection stops working
194
+
195
+ Google grants end on their own sometimes: someone changes their password,
196
+ removes Canopy from their account permissions, or the connection sits unused
197
+ for six months. Calls then fail asking for a reconnect. For the project's
198
+ account, the owner reconnects on the Services tab; for a visitor's, show the
199
+ connect button again. Nothing else needs changing.
200
+
201
+ Disconnecting also tells Google the grant is over, so access stops immediately
202
+ rather than lingering. Visitors can drop their own connection with
203
+ `canopy.google.user().disconnect()`.
204
+
205
+ ## Local development
206
+
207
+ `canopy dev` uses the real connections, so Google works the same on your
208
+ machine as in production — including with `--local`. There is nothing to set up
209
+ locally, and no localhost URLs to register.
210
+
211
+ Because it is real, calls in development touch real data. Sending mail really
212
+ sends it. If that matters, connect a test account on your project's staging
213
+ channel and develop against that.
@@ -34,7 +34,7 @@ canopy-gateway (apps/gateway/src/index.ts)
34
34
  ▼ env.API.fetch(...) (service binding, no public hop)
35
35
  canopy-api (apps/api/src/routes/assets.ts)
36
36
  │ 4. Resolve the project from the stamp + which version is live
37
- │ 5. Sanitize the asset path, SPA-fallback to index.html
37
+ │ 5. Sanitize the asset path; directory index, else SPA fallback
38
38
  │ 6. R2 GET deploys/<project-id>/<version>/<path>
39
39
  ▼
40
40
  R2 bucket "canopy-assets"
@@ -102,14 +102,19 @@ The serving handler is `GET /data/v1/assets/*` in
102
102
  project's deploy history, else 404 — it never silently falls back to live);
103
103
  otherwise the snapshot's `liveAssetVersion` is served, which falls back to
104
104
  the max deployed version for legacy rows with a NULL pointer.
105
- - **Path → R2 key.** The tail after `/assets/` is sanitized (see Security),
106
- defaulting to `index.html`, and mapped to the R2 key
107
- `deploys/<project-id>/<version>/<path>`.
108
- - **SPA fallback + read**: the exact asset is read first, and any miss falls
109
- back to `index.html`, so client-side routes (`/settings`, `/projects/42`) all
110
- resolve to the SPA entry point. The fallback `index.html` is served from a
111
- per-version cache (see [Performance](#performance-review)), so a deep-linked
112
- route doesn't pay a second R2 read on every request.
105
+ - **Path → R2 key.** The tail after `/assets/` is sanitized (see Security)
106
+ and mapped to the R2 key `deploys/<project-id>/<version>/<path>`; the root
107
+ path serves `index.html`.
108
+ - **Directory index + SPA fallback**: the exact asset is read first. On a
109
+ miss, an extensionless path is probed for its own directory index — `/feeds/`
110
+ serves `feeds/index.html` (multi-page static exports), and the slash-less
111
+ `/feeds` 301-redirects to `/feeds/` so the page's relative URLs resolve.
112
+ Only when no directory index exists does the request fall back to the root
113
+ `index.html`, so client-side routes (`/settings`, `/projects/42`) still all
114
+ resolve to the SPA entry point. Both the fallback `index.html` and the
115
+ per-path probe results are served from per-version caches (see
116
+ [Performance](#performance-review)), so a deep-linked route doesn't pay a
117
+ second R2 read on every request.
113
118
 
114
119
  ### How assets are stored & versioned
115
120
 
@@ -1,7 +1,7 @@
1
- import { t as version } from "./package-BBpw-n7k.mjs";
1
+ import { t as version } from "./package-vJXq4RNN.mjs";
2
2
  import { n as siteConfig } from "./site-BUlQR117.mjs";
3
3
  import { i as latestKnownVersion, r as compareSemver } from "./version-check-CcsA1QjI.mjs";
4
- import { n as reportErrors } from "./ui-2mrJcjYC.mjs";
4
+ import { n as reportErrors } from "./ui-B1Px5y7q.mjs";
5
5
  import { defineCommand } from "citty";
6
6
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
7
7
  import { homedir } from "node:os";
@@ -1,5 +1,5 @@
1
1
  import { a as projectDirArgs, c as resolveProject } from "./site-BUlQR117.mjs";
2
- import { c as apiRequest, n as reportErrors } from "./ui-2mrJcjYC.mjs";
2
+ import { c as apiRequest, n as reportErrors } from "./ui-B1Px5y7q.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  //#region src/egress.ts
5
5
  /**
@@ -1,6 +1,6 @@
1
- import { t as version } from "./package-BBpw-n7k.mjs";
1
+ import { t as version } from "./package-vJXq4RNN.mjs";
2
2
  import { a as projectDirArgs, c as resolveProject, n as siteConfig, u as tryReadCanopyJson } from "./site-BUlQR117.mjs";
3
- import { c as apiRequest, l as apiUpload, n as reportErrors, s as apiDownload } from "./ui-2mrJcjYC.mjs";
3
+ import { c as apiRequest, l as apiUpload, n as reportErrors, s as apiDownload } from "./ui-B1Px5y7q.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  import { basename, extname, join } from "node:path";
6
6
  import { readFile, writeFile } from "node:fs/promises";
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as version } from "./package-BBpw-n7k.mjs";
2
+ import { t as version } from "./package-vJXq4RNN.mjs";
3
3
  import { r as siteHeaderLine, t as setInvocationProfile } from "./site-BUlQR117.mjs";
4
4
  import { a as notifyOfUpdate, o as runRefresh } from "./version-check-CcsA1QjI.mjs";
5
5
  import { defineCommand, runMain } from "citty";
@@ -60,29 +60,30 @@ const main = defineCommand({
60
60
  description: "canopy — create and deploy vibecoded projects"
61
61
  },
62
62
  subCommands: {
63
- login: () => import("./login-D7dI8D9N.mjs").then((m) => m.loginCommand),
64
- whoami: () => import("./login-D7dI8D9N.mjs").then((m) => m.whoamiCommand),
65
- logout: () => import("./login-D7dI8D9N.mjs").then((m) => m.logoutCommand),
66
- create: () => import("./projects-C6JQjVqg.mjs").then((m) => m.createCommand),
67
- init: () => import("./projects-C6JQjVqg.mjs").then((m) => m.initCommand),
68
- projects: () => import("./projects-C6JQjVqg.mjs").then((m) => m.projectsCommand),
69
- delete: () => import("./projects-C6JQjVqg.mjs").then((m) => m.deleteCommand),
70
- dev: () => import("./dev-BYxSHUH6.mjs").then((m) => m.devCommand),
71
- deploy: () => import("./deploy-BCtEY1is.mjs").then((m) => m.deployCommand),
72
- promote: () => import("./deploy-BCtEY1is.mjs").then((m) => m.promoteCommand),
73
- staging: () => import("./staging-Bw1D_J8M.mjs").then((m) => m.stagingCommand),
74
- rollback: () => import("./deploy-BCtEY1is.mjs").then((m) => m.rollbackCommand),
75
- download: () => import("./deploy-BCtEY1is.mjs").then((m) => m.downloadCommand),
76
- token: () => import("./token-D7zrIF8G.mjs").then((m) => m.tokenCommand),
77
- egress: () => import("./egress-CN_axsAN.mjs").then((m) => m.egressCommand),
78
- feedback: () => import("./feedback-Cb8ccQ_R.mjs").then((m) => m.feedbackCommand),
79
- db: () => import("./db-8Etjmzxh.mjs").then((m) => m.dbCommand),
80
- usage: () => import("./usage-CX1Izj_l.mjs").then((m) => m.usageCommand),
81
- open: () => import("./open-DRPp4QJg.mjs").then((m) => m.openCommand),
82
- docs: () => import("./docs-BV9qhLJx.mjs").then((m) => m.docsCommand),
83
- skill: () => import("./skill-DJHlTEhS.mjs").then((m) => m.skillCommand),
84
- upgrade: () => import("./upgrade-B86Ao6T5.mjs").then((m) => m.upgradeCommand),
85
- logs: () => import("./logs-DNiZquki.mjs").then((m) => m.logsCommand)
63
+ login: () => import("./login-BDTO3nVw.mjs").then((m) => m.loginCommand),
64
+ whoami: () => import("./login-BDTO3nVw.mjs").then((m) => m.whoamiCommand),
65
+ logout: () => import("./login-BDTO3nVw.mjs").then((m) => m.logoutCommand),
66
+ create: () => import("./projects-MMbv7MmK.mjs").then((m) => m.createCommand),
67
+ init: () => import("./projects-MMbv7MmK.mjs").then((m) => m.initCommand),
68
+ projects: () => import("./projects-MMbv7MmK.mjs").then((m) => m.projectsCommand),
69
+ delete: () => import("./projects-MMbv7MmK.mjs").then((m) => m.deleteCommand),
70
+ dev: () => import("./dev-BqLvttKc.mjs").then((m) => m.devCommand),
71
+ deploy: () => import("./deploy-CnxJ4TE6.mjs").then((m) => m.deployCommand),
72
+ promote: () => import("./deploy-CnxJ4TE6.mjs").then((m) => m.promoteCommand),
73
+ staging: () => import("./staging-D8G4amjO.mjs").then((m) => m.stagingCommand),
74
+ rollback: () => import("./deploy-CnxJ4TE6.mjs").then((m) => m.rollbackCommand),
75
+ download: () => import("./deploy-CnxJ4TE6.mjs").then((m) => m.downloadCommand),
76
+ token: () => import("./token-BUxgRJVA.mjs").then((m) => m.tokenCommand),
77
+ egress: () => import("./egress-BFSwa8Uu.mjs").then((m) => m.egressCommand),
78
+ schedule: () => import("./schedule-Cq9K0rnE.mjs").then((m) => m.scheduleCommand),
79
+ feedback: () => import("./feedback-BMX_d4KZ.mjs").then((m) => m.feedbackCommand),
80
+ db: () => import("./db-DKnJQPm5.mjs").then((m) => m.dbCommand),
81
+ usage: () => import("./usage-DjNngyvy.mjs").then((m) => m.usageCommand),
82
+ open: () => import("./open-C5x3tX6I.mjs").then((m) => m.openCommand),
83
+ docs: () => import("./docs-BBMVxVta.mjs").then((m) => m.docsCommand),
84
+ skill: () => import("./skill-Zq9aIPKf.mjs").then((m) => m.skillCommand),
85
+ upgrade: () => import("./upgrade-ES8u8_Pn.mjs").then((m) => m.upgradeCommand),
86
+ logs: () => import("./logs-C53y2eAS.mjs").then((m) => m.logsCommand)
86
87
  }
87
88
  });
88
89
  if (process.argv[2] !== "upgrade") notifyOfUpdate(version);