@mediamonks/canopy-cli 0.7.25 → 0.7.26

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 (34) hide show
  1. package/dist/{db-jmYaum8F.mjs → db-DVMwCLlU.mjs} +2 -2
  2. package/dist/{deploy-CYKiBnrg.mjs → deploy-CTMcwHhI.mjs} +2 -2
  3. package/dist/dev-BOvMCaML.mjs +2 -0
  4. package/dist/{dev-BanjQNiD.mjs → dev-BvjbJcse.mjs} +4 -4
  5. package/dist/docs/cli-dev.md +1 -1
  6. package/dist/docs/cli-secrets.md +45 -0
  7. package/dist/docs/dev.md +3 -3
  8. package/dist/docs/index.json +5 -0
  9. package/dist/docs/platform-google.md +15 -8
  10. package/dist/docs/porting-nextjs.md +19 -19
  11. package/dist/{docs-xRbVshXp.mjs → docs-x3hMtUzT.mjs} +2 -2
  12. package/dist/{egress-BjN-7vil.mjs → egress-Cn-P46O3.mjs} +1 -1
  13. package/dist/{feedback-CGjOAHON.mjs → feedback-C9m3dNkJ.mjs} +2 -2
  14. package/dist/index.mjs +26 -25
  15. package/dist/{local-DvzG4XUB.mjs → local-laAZEDVJ.mjs} +2 -2
  16. package/dist/{login-COZ7Xcm0.mjs → login-C4MDP5NM.mjs} +1 -1
  17. package/dist/{logs-B_YoflCq.mjs → logs-CfDINw7U.mjs} +1 -1
  18. package/dist/{migrations-Btzr9_q7.mjs → migrations-B9fXxMDj.mjs} +1 -1
  19. package/dist/{open-D2hH933K.mjs → open-rS1ARzOy.mjs} +1 -1
  20. package/dist/{package-vZ0xH49Y.mjs → package-CBso2611.mjs} +1 -1
  21. package/dist/{projects-CLs6MEF7.mjs → projects-Dmm_976k.mjs} +1 -1
  22. package/dist/{schedule-B8zA8WlG.mjs → schedule-Buu71RWQ.mjs} +1 -1
  23. package/dist/secrets-BD3DaTih.mjs +121 -0
  24. package/dist/{skill-PoaZrCNy.mjs → skill-DmVlZXgv.mjs} +2 -2
  25. package/dist/{staging-pZpdZuj_.mjs → staging-CKEoDnac.mjs} +1 -1
  26. package/dist/templates/worker/.dev.vars.example +1 -1
  27. package/dist/templates/worker/AGENTS.md +1 -1
  28. package/dist/templates/worker/worker/index.ts +1 -1
  29. package/dist/{token-BKwGa10J.mjs → token-Dp2CoIYj.mjs} +1 -1
  30. package/dist/{ui-CUNkis89.mjs → ui-DR-siu2S.mjs} +52 -5
  31. package/dist/{upgrade-DjrNZ-B4.mjs → upgrade-C7u8pxeV.mjs} +2 -2
  32. package/dist/{usage-UmzU6sGp.mjs → usage-81JSiWIx.mjs} +1 -1
  33. package/package.json +1 -1
  34. package/dist/dev-BAcZi226.mjs +0 -2
@@ -1,6 +1,6 @@
1
1
  import { c as resolveProject, o as projectFlagArgs } from "./site-DM2J10CB.mjs";
2
- import { a as unwrap, d as withContext, i as requireTty, l as apiRequest, r as reportErrors, s as apiDataRequest } from "./ui-CUNkis89.mjs";
3
- import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-Btzr9_q7.mjs";
2
+ import { a as unwrap, d as withContext, i as requireTty, l as apiRequest, r as reportErrors, s as apiDataRequest } from "./ui-DR-siu2S.mjs";
3
+ import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-B9fXxMDj.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-DM2J10CB.mjs";
2
- import { a as unwrap, c as apiDownload, d as withContext, l as apiRequest, r as reportErrors, s as apiDataRequest, t as formatDateTime } from "./ui-CUNkis89.mjs";
3
- import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-Btzr9_q7.mjs";
2
+ import { a as unwrap, c as apiDownload, d as withContext, l as apiRequest, r as reportErrors, s as apiDataRequest, t as formatDateTime } from "./ui-DR-siu2S.mjs";
3
+ import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-B9fXxMDj.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";
@@ -0,0 +1,2 @@
1
+ import { t as devCommand } from "./dev-BvjbJcse.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-DM2J10CB.mjs";
2
- import { d as withContext, l as apiRequest, o as ApiError, r as reportErrors, t as formatDateTime } from "./ui-CUNkis89.mjs";
3
- import { r as readMigrationFiles } from "./migrations-Btzr9_q7.mjs";
2
+ import { d as withContext, l as apiRequest, o as ApiError, r as reportErrors, t as formatDateTime } from "./ui-DR-siu2S.mjs";
3
+ import { r as readMigrationFiles } from "./migrations-B9fXxMDj.mjs";
4
4
  import { n as stampWorkerHeaders } from "./src-CwSKZwNK.mjs";
5
5
  import { createRequire } from "node:module";
6
6
  import { defineCommand } from "citty";
@@ -176,7 +176,7 @@ async function devRemote(flags, slug) {
176
176
  async function devLocal(flags, slug) {
177
177
  let dump;
178
178
  if (flags.pull) {
179
- const { fetchProjectDump } = await import("./local-DvzG4XUB.mjs");
179
+ const { fetchProjectDump } = await import("./local-laAZEDVJ.mjs");
180
180
  console.log(`Pulling a fresh export of ${slug}${flags.staging ? " [staging]" : ""}…`);
181
181
  dump = await fetchProjectDump(slug, flags.staging ? "staging" : "live");
182
182
  }
@@ -191,7 +191,7 @@ async function devLocal(flags, slug) {
191
191
  const reason = err instanceof ApiError ? err.explain().why : String(err);
192
192
  console.log(`Note: ai/services will be unavailable — could not reach the platform (${reason}).`);
193
193
  }
194
- const { createSandbox } = await import("./local-DvzG4XUB.mjs");
194
+ const { createSandbox } = await import("./local-laAZEDVJ.mjs");
195
195
  const sandbox = await createSandbox({
196
196
  slug,
197
197
  persistDir: join(flags.dir, ".canopy", "local"),
@@ -47,7 +47,7 @@ To tick a schedule by hand, post its name to the same path the platform uses:
47
47
  curl -X POST localhost:5173/__canopy/scheduled -d '{"name":"nightly-digest"}'
48
48
  ```
49
49
 
50
- **Worker secrets.** Secrets you set in the dashboard are never downloaded — the platform can't hand a stored value back, and your laptop is not where a live API key belongs. Put local values in a `.dev.vars` file next to `canopy.json` instead:
50
+ **Worker secrets.** Secrets you set in the dashboard or with [`canopy secrets set`](https://docs.canopy.website/cli/secrets) are never downloaded — the platform can't hand a stored value back, and your laptop is not where a live API key belongs. Put local values in a `.dev.vars` file next to `canopy.json` instead:
51
51
 
52
52
  ```
53
53
  STRIPE_KEY=sk_test_local
@@ -0,0 +1,45 @@
1
+ ---
2
+ title: canopy secrets
3
+ description: Set, list, and remove the project's worker secrets from the terminal.
4
+ ---
5
+
6
+ `canopy secrets` manages the secrets your backend worker reads as `c.env.NAME`
7
+ — API keys, signing secrets, anything the browser must never see. It does the
8
+ same job as the dashboard's Secrets panel, so a first deploy can be scripted
9
+ end to end: create the project, allow its hosts, set its secrets, deploy.
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ canopy secrets list
15
+ printf %s "$STRIPE_SECRET" | canopy secrets set STRIPE_SECRET
16
+ canopy secrets set STRIPE_SECRET --from-env STRIPE_SECRET
17
+ canopy secrets set STRIPE_SECRET # hidden prompt in a terminal
18
+ canopy secrets remove STRIPE_SECRET
19
+ ```
20
+
21
+ ## Arguments & flags
22
+
23
+ All subcommands resolve the project the usual way — `canopy.json` in the
24
+ target directory, or `--project <slug>` to override — and honor the global
25
+ `--profile <site>` / `CANOPY_PROFILE`.
26
+
27
+ - `list` — print the secret names and when each was last set. Values are
28
+ never shown.
29
+ - `set <name>` — store a value. Names are `UPPER_SNAKE_CASE`, up to 64
30
+ characters. The value comes from **stdin** when piped (one trailing newline
31
+ is trimmed, so `echo` works too), from `--from-env <VAR>` to read an
32
+ environment variable, or from a hidden prompt when you run it in a terminal.
33
+ There is deliberately no `--value` flag: a value on the command line would
34
+ end up in shell history and process lists.
35
+ - `remove <name>` — delete a secret. The worker stops seeing it right away.
36
+
37
+ ## How it works
38
+
39
+ Secrets live on the platform, encrypted, per project. Setting or removing one
40
+ re-rolls the running worker immediately — no deploy needed. The live and
41
+ staging channels share one set of secrets, like the egress allowlist. Values
42
+ travel one way: once stored they can't be read back by anyone, including
43
+ `canopy dev`, which is why local development uses a `.dev.vars` file with
44
+ stand-in values (see [`canopy dev`](https://docs.canopy.website/cli/dev)). A project holds at most 64
45
+ secrets of up to 8 KiB each.
package/dist/docs/dev.md CHANGED
@@ -53,9 +53,9 @@ Projects with a `worker` entry in `canopy.json` get their backend locally too:
53
53
  production (`env.CANOPY`, `env.DB`, `env.BLOB`, `env.USER`). Edit and refresh —
54
54
  there's no build step.
55
55
 
56
- Secrets are the one thing that doesn't come from the dashboard. Stored values
57
- are never downloadable, so put local ones in a `.dev.vars` file next to
58
- `canopy.json`:
56
+ Secrets are the one thing that doesn't come from the platform. Stored values
57
+ (set in the dashboard or with `canopy secrets set`) are never downloadable, so
58
+ put local ones in a `.dev.vars` file next to `canopy.json`:
59
59
 
60
60
  ```
61
61
  STRIPE_KEY=sk_test_local
@@ -251,6 +251,11 @@
251
251
  "title": "canopy egress",
252
252
  "summary": "Manage which outbound hosts the project's backend worker may fetch."
253
253
  },
254
+ {
255
+ "slug": "cli-secrets",
256
+ "title": "canopy secrets",
257
+ "summary": "Set, list, and remove the project's worker secrets from the terminal."
258
+ },
254
259
  {
255
260
  "slug": "cli-schedule",
256
261
  "title": "canopy schedule",
@@ -97,6 +97,12 @@ so is a write using a reading-only permission.
97
97
 
98
98
  Drive is deliberately limited to files your project created or that someone
99
99
  explicitly opened with it — connecting Drive does not expose a whole Drive.
100
+ The one exception is **Copy files it is given**: hand your project the id of a
101
+ file the account can open (a template deck, say) and it may copy that one file.
102
+ The copy is a file your project made, so with _Files this project makes_ it can
103
+ open and change the copy from then on — the original is never touched. It cannot
104
+ list, search or read anything else. Because Google has no copy-only permission,
105
+ this one counts as wider access and needs a Canopy admin to unlock it.
100
106
 
101
107
  ### Wider permissions
102
108
 
@@ -145,14 +151,15 @@ reading-only permission is refused before it reaches Google.
145
151
 
146
152
  **Drive**
147
153
 
148
- | Permission | What it allows |
149
- | --------------------------------------------- | ----------------------------------------------------------------------------- |
150
- | Files this project makes | Create files, and reopen the ones this project made or someone picked for it. |
151
- | Its own config folder | A hidden folder only this project can see. Invisible in the account's Drive. |
152
- | Recordings and transcripts **(wider access)** | Read the recordings and transcripts Meet saved to Drive. |
153
- | Read the whole Drive _(under consideration)_ | Read and download every file the account can see. |
154
- | Read all file names _(under consideration)_ | Names and metadata of every file the account can see, without contents. |
155
- | Full Drive access _(under consideration)_ | Read, change and delete every file the account can see. |
154
+ | Permission | What it allows |
155
+ | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
156
+ | Files this project makes | Create files, and reopen the ones this project made or someone picked for it. |
157
+ | Its own config folder | A hidden folder only this project can see. Invisible in the account's Drive. |
158
+ | Copy files it is given **(wider access)** | Copy a file the account can open, by id, into a file this project owns. Cannot list, read or change anything else. |
159
+ | Recordings and transcripts **(wider access)** | Read the recordings and transcripts Meet saved to Drive. |
160
+ | Read the whole Drive _(under consideration)_ | Read and download every file the account can see. |
161
+ | Read all file names _(under consideration)_ | Names and metadata of every file the account can see, without contents. |
162
+ | Full Drive access _(under consideration)_ | Read, change and delete every file the account can see. |
156
163
 
157
164
  **Gmail**
158
165
 
@@ -62,24 +62,24 @@ together as a single version, with rollback.
62
62
  List everything in the app that runs on the server. Each item lands in one of
63
63
  three buckets:
64
64
 
65
- | Next.js feature | Where it goes on Canopy |
66
- | ----------------------------------------------- | -------------------------------------------------------------------------------- |
67
- | Pages, layouts, client components | Static build (unchanged in spirit) |
68
- | Server components fetching data at request time | Client-side fetching, or a worker endpoint |
69
- | Route handlers (`app/api/*/route.ts`) | Worker endpoints under `/api/*` |
70
- | Server actions | Explicit worker endpoints, called with `fetch` from the client |
71
- | Middleware | Usually deleted (auth is platform-provided); anything else moves into the worker |
72
- | NextAuth / auth middleware | Deleted — use `canopy.auth.user()` (client) or `env.USER` (worker) |
73
- | Database (Prisma, Postgres, …) | `canopy.db` — per-project database, document API + SQL escape hatch |
74
- | File uploads (S3, …) | `canopy.blob` — per-project file storage with signed downloads |
75
- | AI SDK / OpenAI client | `canopy.ai.chat()`, or point the Vercel AI SDK at `canopy.ai.endpoint()` |
76
- | `NEXT_PUBLIC_*` env vars | Build-time env vars in your CI — the platform doesn't inject build config |
77
- | Server-side env vars / secrets | Project secrets (dashboard Settings) — injected into the worker's `env` |
78
- | Cron (`vercel.json` crons) | [Schedules](https://docs.canopy.website/platform/schedules) — delivered to the worker's `scheduled()` |
79
- | ISR / `revalidate` / caching | Dropped — at internal-tool scale, fetch fresh or cache in the client |
80
- | `next/image` optimization | Plain `<img>`, or optimize at build time |
81
- | Custom domain | Not available — projects live at `<slug>.canopy.<domain>` behind sign-in |
82
- | Queues / background jobs | Not supported — restructure around request/response plus schedules |
65
+ | Next.js feature | Where it goes on Canopy |
66
+ | ----------------------------------------------- | ----------------------------------------------------------------------------------------------- |
67
+ | Pages, layouts, client components | Static build (unchanged in spirit) |
68
+ | Server components fetching data at request time | Client-side fetching, or a worker endpoint |
69
+ | Route handlers (`app/api/*/route.ts`) | Worker endpoints under `/api/*` |
70
+ | Server actions | Explicit worker endpoints, called with `fetch` from the client |
71
+ | Middleware | Usually deleted (auth is platform-provided); anything else moves into the worker |
72
+ | NextAuth / auth middleware | Deleted — use `canopy.auth.user()` (client) or `env.USER` (worker) |
73
+ | Database (Prisma, Postgres, …) | `canopy.db` — per-project database, document API + SQL escape hatch |
74
+ | File uploads (S3, …) | `canopy.blob` — per-project file storage with signed downloads |
75
+ | AI SDK / OpenAI client | `canopy.ai.chat()`, or point the Vercel AI SDK at `canopy.ai.endpoint()` |
76
+ | `NEXT_PUBLIC_*` env vars | Build-time env vars in your CI — the platform doesn't inject build config |
77
+ | Server-side env vars / secrets | Project secrets (`canopy secrets set` or dashboard Settings) — injected into the worker's `env` |
78
+ | Cron (`vercel.json` crons) | [Schedules](https://docs.canopy.website/platform/schedules) — delivered to the worker's `scheduled()` |
79
+ | ISR / `revalidate` / caching | Dropped — at internal-tool scale, fetch fresh or cache in the client |
80
+ | `next/image` optimization | Plain `<img>`, or optimize at build time |
81
+ | Custom domain | Not available — projects live at `<slug>.canopy.<domain>` behind sign-in |
82
+ | Queues / background jobs | Not supported — restructure around request/response plus schedules |
83
83
 
84
84
  If the last two rows are deal-breakers, stop here: see
85
85
  [When not to port](#when-not-to-port).
@@ -253,6 +253,6 @@ past greenhouse size (see [Use cases](https://docs.canopy.website/overview/use-c
253
253
  - [ ] Worker: route handlers + server actions → `/api/*` endpoints in `worker/index.ts`
254
254
  - [ ] Data: ORM → `canopy.db`; uploads → `canopy.blob`; AI clients → `canopy.ai`
255
255
  - [ ] Delete NextAuth/middleware; read identity from `canopy.auth.user()` / `env.USER`
256
- - [ ] Secrets into dashboard Settings; `NEXT_PUBLIC_*` into your CI build
256
+ - [ ] Secrets via `canopy secrets set` (or dashboard Settings); `NEXT_PUBLIC_*` into your CI build
257
257
  - [ ] Crons into Settings → Schedules, handled in `scheduled()`
258
258
  - [ ] `canopy.json` with `build`, `dist`, `worker` — then `canopy deploy`
@@ -1,7 +1,7 @@
1
- import { t as version } from "./package-vZ0xH49Y.mjs";
1
+ import { t as version } from "./package-CBso2611.mjs";
2
2
  import { n as siteConfig } from "./site-DM2J10CB.mjs";
3
3
  import { i as latestKnownVersion, r as compareSemver } from "./version-check-CcsA1QjI.mjs";
4
- import { r as reportErrors } from "./ui-CUNkis89.mjs";
4
+ import { r as reportErrors } from "./ui-DR-siu2S.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-DM2J10CB.mjs";
2
- import { a as unwrap, d as withContext, i as requireTty, l as apiRequest, r as reportErrors, t as formatDateTime } from "./ui-CUNkis89.mjs";
2
+ import { a as unwrap, d as withContext, i as requireTty, l as apiRequest, r as reportErrors, t as formatDateTime } from "./ui-DR-siu2S.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import * as p from "@clack/prompts";
5
5
  //#region src/egress.ts
@@ -1,6 +1,6 @@
1
- import { t as version } from "./package-vZ0xH49Y.mjs";
1
+ import { t as version } from "./package-CBso2611.mjs";
2
2
  import { a as projectDirArgs, c as resolveProject, d as tryResolveProject, n as siteConfig, u as tryReadCanopyJson } from "./site-DM2J10CB.mjs";
3
- import { c as apiDownload, l as apiRequest, r as reportErrors, t as formatDateTime, u as apiUpload } from "./ui-CUNkis89.mjs";
3
+ import { c as apiDownload, l as apiRequest, r as reportErrors, t as formatDateTime, u as apiUpload } from "./ui-DR-siu2S.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-vZ0xH49Y.mjs";
2
+ import { t as version } from "./package-CBso2611.mjs";
3
3
  import { r as siteHeaderLine, t as setInvocationProfile } from "./site-DM2J10CB.mjs";
4
4
  import { a as notifyOfUpdate, o as runRefresh } from "./version-check-CcsA1QjI.mjs";
5
5
  import { defineCommand, runMain } from "citty";
@@ -60,30 +60,31 @@ const main = defineCommand({
60
60
  description: "canopy — create and deploy vibecoded projects"
61
61
  },
62
62
  subCommands: {
63
- login: () => import("./login-COZ7Xcm0.mjs").then((m) => m.loginCommand),
64
- whoami: () => import("./login-COZ7Xcm0.mjs").then((m) => m.whoamiCommand),
65
- logout: () => import("./login-COZ7Xcm0.mjs").then((m) => m.logoutCommand),
66
- create: () => import("./projects-CLs6MEF7.mjs").then((m) => m.createCommand),
67
- init: () => import("./projects-CLs6MEF7.mjs").then((m) => m.initCommand),
68
- projects: () => import("./projects-CLs6MEF7.mjs").then((m) => m.projectsCommand),
69
- delete: () => import("./projects-CLs6MEF7.mjs").then((m) => m.deleteCommand),
70
- dev: () => import("./dev-BAcZi226.mjs").then((m) => m.devCommand),
71
- deploy: () => import("./deploy-CYKiBnrg.mjs").then((m) => m.deployCommand),
72
- promote: () => import("./deploy-CYKiBnrg.mjs").then((m) => m.promoteCommand),
73
- staging: () => import("./staging-pZpdZuj_.mjs").then((m) => m.stagingCommand),
74
- rollback: () => import("./deploy-CYKiBnrg.mjs").then((m) => m.rollbackCommand),
75
- download: () => import("./deploy-CYKiBnrg.mjs").then((m) => m.downloadCommand),
76
- token: () => import("./token-BKwGa10J.mjs").then((m) => m.tokenCommand),
77
- egress: () => import("./egress-BjN-7vil.mjs").then((m) => m.egressCommand),
78
- schedule: () => import("./schedule-B8zA8WlG.mjs").then((m) => m.scheduleCommand),
79
- feedback: () => import("./feedback-CGjOAHON.mjs").then((m) => m.feedbackCommand),
80
- db: () => import("./db-jmYaum8F.mjs").then((m) => m.dbCommand),
81
- usage: () => import("./usage-UmzU6sGp.mjs").then((m) => m.usageCommand),
82
- open: () => import("./open-D2hH933K.mjs").then((m) => m.openCommand),
83
- docs: () => import("./docs-xRbVshXp.mjs").then((m) => m.docsCommand),
84
- skill: () => import("./skill-PoaZrCNy.mjs").then((m) => m.skillCommand),
85
- upgrade: () => import("./upgrade-DjrNZ-B4.mjs").then((m) => m.upgradeCommand),
86
- logs: () => import("./logs-B_YoflCq.mjs").then((m) => m.logsCommand)
63
+ login: () => import("./login-C4MDP5NM.mjs").then((m) => m.loginCommand),
64
+ whoami: () => import("./login-C4MDP5NM.mjs").then((m) => m.whoamiCommand),
65
+ logout: () => import("./login-C4MDP5NM.mjs").then((m) => m.logoutCommand),
66
+ create: () => import("./projects-Dmm_976k.mjs").then((m) => m.createCommand),
67
+ init: () => import("./projects-Dmm_976k.mjs").then((m) => m.initCommand),
68
+ projects: () => import("./projects-Dmm_976k.mjs").then((m) => m.projectsCommand),
69
+ delete: () => import("./projects-Dmm_976k.mjs").then((m) => m.deleteCommand),
70
+ dev: () => import("./dev-BOvMCaML.mjs").then((m) => m.devCommand),
71
+ deploy: () => import("./deploy-CTMcwHhI.mjs").then((m) => m.deployCommand),
72
+ promote: () => import("./deploy-CTMcwHhI.mjs").then((m) => m.promoteCommand),
73
+ staging: () => import("./staging-CKEoDnac.mjs").then((m) => m.stagingCommand),
74
+ rollback: () => import("./deploy-CTMcwHhI.mjs").then((m) => m.rollbackCommand),
75
+ download: () => import("./deploy-CTMcwHhI.mjs").then((m) => m.downloadCommand),
76
+ token: () => import("./token-Dp2CoIYj.mjs").then((m) => m.tokenCommand),
77
+ egress: () => import("./egress-Cn-P46O3.mjs").then((m) => m.egressCommand),
78
+ secrets: () => import("./secrets-BD3DaTih.mjs").then((m) => m.secretsCommand),
79
+ schedule: () => import("./schedule-Buu71RWQ.mjs").then((m) => m.scheduleCommand),
80
+ feedback: () => import("./feedback-C9m3dNkJ.mjs").then((m) => m.feedbackCommand),
81
+ db: () => import("./db-DVMwCLlU.mjs").then((m) => m.dbCommand),
82
+ usage: () => import("./usage-81JSiWIx.mjs").then((m) => m.usageCommand),
83
+ open: () => import("./open-rS1ARzOy.mjs").then((m) => m.openCommand),
84
+ docs: () => import("./docs-x3hMtUzT.mjs").then((m) => m.docsCommand),
85
+ skill: () => import("./skill-DmVlZXgv.mjs").then((m) => m.skillCommand),
86
+ upgrade: () => import("./upgrade-C7u8pxeV.mjs").then((m) => m.upgradeCommand),
87
+ logs: () => import("./logs-CfDINw7U.mjs").then((m) => m.logsCommand)
87
88
  }
88
89
  });
89
90
  if (process.argv[2] !== "upgrade") notifyOfUpdate(version);
@@ -1,6 +1,6 @@
1
- import { s as apiDataRequest } from "./ui-CUNkis89.mjs";
1
+ import { s as apiDataRequest } from "./ui-DR-siu2S.mjs";
2
2
  import { a as blobRouter, i as META_COLUMNS, r as dbRouter } from "./src-CwSKZwNK.mjs";
3
- import { r as rewriteApiPath } from "./dev-BanjQNiD.mjs";
3
+ import { r as rewriteApiPath } from "./dev-BvjbJcse.mjs";
4
4
  import { createRequire } from "node:module";
5
5
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
6
6
  import { dirname, join, relative, sep } from "node:path";
@@ -1,5 +1,5 @@
1
1
  import { _ as writeConfigFile, f as clearAllProfileTokens, g as upsertProfileAuth, h as setActiveProfile, m as readConfigFile, n as siteConfig, p as listBakedProfiles, v as getBakedProfile, y as isProfileId } from "./site-DM2J10CB.mjs";
2
- import { n as openBrowser, r as reportErrors } from "./ui-CUNkis89.mjs";
2
+ import { n as openBrowser, r as reportErrors } from "./ui-DR-siu2S.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import { hostname } from "node:os";
5
5
  import * as p from "@clack/prompts";
@@ -1,5 +1,5 @@
1
1
  import { c as resolveProject, o as projectFlagArgs } from "./site-DM2J10CB.mjs";
2
- import { d as withContext, l as apiRequest, r as reportErrors } from "./ui-CUNkis89.mjs";
2
+ import { d as withContext, l as apiRequest, r as reportErrors } from "./ui-DR-siu2S.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  //#region src/logs.ts
5
5
  /**
@@ -1,5 +1,5 @@
1
1
  import { u as tryReadCanopyJson } from "./site-DM2J10CB.mjs";
2
- import { s as apiDataRequest } from "./ui-CUNkis89.mjs";
2
+ import { s as apiDataRequest } from "./ui-DR-siu2S.mjs";
3
3
  import { readFileSync, readdirSync } from "node:fs";
4
4
  import { join, resolve } from "node:path";
5
5
  //#region src/migrations.ts
@@ -1,5 +1,5 @@
1
1
  import { a as projectDirArgs, c as resolveProject, l as stagingUrl, s as projectUrl } from "./site-DM2J10CB.mjs";
2
- import { n as openBrowser, r as reportErrors } from "./ui-CUNkis89.mjs";
2
+ import { n as openBrowser, r as reportErrors } from "./ui-DR-siu2S.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  //#region src/open.ts
5
5
  /**
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.7.25";
2
+ var version = "0.7.26";
3
3
  //#endregion
4
4
  export { version as t };
@@ -1,5 +1,5 @@
1
1
  import { n as siteConfig, s as projectUrl, u as tryReadCanopyJson } from "./site-DM2J10CB.mjs";
2
- import { a as unwrap, i as requireTty, l as apiRequest, r as reportErrors } from "./ui-CUNkis89.mjs";
2
+ import { a as unwrap, i as requireTty, l as apiRequest, r as reportErrors } from "./ui-DR-siu2S.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
5
5
  import { join, relative, resolve, sep } from "node:path";
@@ -1,5 +1,5 @@
1
1
  import { c as resolveProject, o as projectFlagArgs } from "./site-DM2J10CB.mjs";
2
- import { d as withContext, l as apiRequest, r as reportErrors, t as formatDateTime } from "./ui-CUNkis89.mjs";
2
+ import { d as withContext, l as apiRequest, r as reportErrors, t as formatDateTime } from "./ui-DR-siu2S.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  //#region src/schedule.ts
5
5
  /**
@@ -0,0 +1,121 @@
1
+ import { a as projectDirArgs, c as resolveProject } from "./site-DM2J10CB.mjs";
2
+ import { a as unwrap, d as withContext, l as apiRequest, r as reportErrors, t as formatDateTime } from "./ui-DR-siu2S.mjs";
3
+ import { defineCommand } from "citty";
4
+ import * as p from "@clack/prompts";
5
+ //#region src/secrets.ts
6
+ /**
7
+ * canopy secrets — manage the project's worker secrets from the terminal so a
8
+ * first deploy can be scripted end to end (create → egress → secrets → deploy).
9
+ * Values travel one way: set via stdin or an environment variable (never a
10
+ * flag, so nothing lands in shell history), listed by name only, and never
11
+ * echoed or downloaded — the same rule the dashboard follows. Secrets are
12
+ * shared by the live and staging channels (docs/14 §4.6).
13
+ */
14
+ /**
15
+ * A piped value usually ends in the newline `echo` or a heredoc appends; a
16
+ * secret never legitimately does. Strip exactly one trailing line break and
17
+ * leave everything else verbatim (leading/inner whitespace may be meaningful).
18
+ */
19
+ function normalizeSecretValue(raw) {
20
+ return raw.replace(/\r?\n$/, "");
21
+ }
22
+ async function readStdin() {
23
+ const chunks = [];
24
+ for await (const chunk of process.stdin) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
25
+ return Buffer.concat(chunks).toString("utf8");
26
+ }
27
+ /** Resolve the value: --from-env, then piped stdin, then a hidden prompt on a TTY. */
28
+ async function resolveValue(name, fromEnv) {
29
+ if (fromEnv !== void 0) {
30
+ const value = process.env[fromEnv];
31
+ if (value === void 0) throw new Error(`environment variable ${fromEnv} is not set (nothing to store for ${name}).`);
32
+ return value;
33
+ }
34
+ if (!process.stdin.isTTY) return normalizeSecretValue(await readStdin());
35
+ return unwrap(await p.password({
36
+ message: `Value for ${name} (input is hidden)`,
37
+ validate: (v) => v ? void 0 : "A secret value cannot be empty."
38
+ }));
39
+ }
40
+ const secretsCommand = defineCommand({
41
+ meta: {
42
+ name: "secrets",
43
+ description: "Manage the project's worker secrets (set / list / remove — values never shown)"
44
+ },
45
+ subCommands: {
46
+ list: defineCommand({
47
+ meta: {
48
+ name: "list",
49
+ description: "List the project's secret names (values are never shown)"
50
+ },
51
+ args: { ...projectDirArgs },
52
+ run: reportErrors("secrets list", async ({ args }) => {
53
+ const { slug } = resolveProject(args);
54
+ const { secrets } = await apiRequest("GET", `/projects/${slug}/secrets`);
55
+ if (secrets.length === 0) {
56
+ console.log(`No secrets set for ${slug} — add one with \`canopy secrets set NAME\`.`);
57
+ return;
58
+ }
59
+ const width = Math.max(...secrets.map((s) => s.key.length));
60
+ for (const s of secrets) console.log(`${s.key.padEnd(width)} updated ${formatDateTime(s.updated_at)}`);
61
+ })
62
+ }),
63
+ set: defineCommand({
64
+ meta: {
65
+ name: "set",
66
+ description: "Set a secret — value from stdin (`printf %s \"$V\" | canopy secrets set NAME`), --from-env, or a hidden prompt"
67
+ },
68
+ args: {
69
+ name: {
70
+ type: "positional",
71
+ description: "Secret name (UPPER_SNAKE_CASE, e.g. STRIPE_SECRET)",
72
+ required: true
73
+ },
74
+ "from-env": {
75
+ type: "string",
76
+ description: "Read the value from this environment variable instead of stdin"
77
+ },
78
+ ...projectDirArgs
79
+ },
80
+ run: reportErrors("secrets set", async ({ args }) => {
81
+ const { slug } = resolveProject(args);
82
+ const name = args.name.trim();
83
+ const value = await resolveValue(name, args["from-env"]);
84
+ if (value.length === 0) {
85
+ console.error(`No value provided for ${name} — pipe it on stdin or pass --from-env VAR.`);
86
+ process.exit(1);
87
+ }
88
+ await withContext(apiRequest("PUT", `/projects/${slug}/secrets/${name}`, { value }), {
89
+ action: `set secret ${name} on ${slug}`,
90
+ fixes: { not_supported_on_target: "This site has no worker runtime, so secrets have nowhere to go — set them on a site that runs workers (e.g. `--profile main`)." }
91
+ });
92
+ console.log(`Set ${name} for ${slug} — running workers pick it up now.`);
93
+ })
94
+ }),
95
+ remove: defineCommand({
96
+ meta: {
97
+ name: "remove",
98
+ description: "Remove a secret"
99
+ },
100
+ args: {
101
+ name: {
102
+ type: "positional",
103
+ description: "Secret name (as shown by `list`)",
104
+ required: true
105
+ },
106
+ ...projectDirArgs
107
+ },
108
+ run: reportErrors("secrets remove", async ({ args }) => {
109
+ const { slug } = resolveProject(args);
110
+ const name = args.name.trim();
111
+ await withContext(apiRequest("DELETE", `/projects/${slug}/secrets/${name}`), {
112
+ action: `remove secret ${name} from ${slug}`,
113
+ fixes: { not_found: `${name} is not set on ${slug} (see \`canopy secrets list\`), or your account can't access the project.` }
114
+ });
115
+ console.log(`Removed ${name} from ${slug} — running workers no longer see it.`);
116
+ })
117
+ })
118
+ }
119
+ });
120
+ //#endregion
121
+ export { secretsCommand };
@@ -1,5 +1,5 @@
1
- import { t as version } from "./package-vZ0xH49Y.mjs";
2
- import { r as reportErrors } from "./ui-CUNkis89.mjs";
1
+ import { t as version } from "./package-CBso2611.mjs";
2
+ import { r as reportErrors } from "./ui-DR-siu2S.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import { cpSync, existsSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
5
5
  import { homedir } from "node:os";
@@ -1,5 +1,5 @@
1
1
  import { c as resolveProject, l as stagingUrl, o as projectFlagArgs, s as projectUrl } from "./site-DM2J10CB.mjs";
2
- import { a as unwrap, d as withContext, i as requireTty, l as apiRequest, r as reportErrors, t as formatDateTime } from "./ui-CUNkis89.mjs";
2
+ import { a as unwrap, d as withContext, i as requireTty, l as apiRequest, r as reportErrors, t as formatDateTime } from "./ui-DR-siu2S.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import * as p from "@clack/prompts";
5
5
  //#region src/staging.ts
@@ -1,6 +1,6 @@
1
1
  # Local worker secrets for `canopy dev` — copy to .dev.vars (gitignored).
2
2
  #
3
- # Dashboard secrets (Settings → Secrets) are never downloaded: the platform
3
+ # Project secrets (`canopy secrets set NAME` or Settings → Secrets) are never downloaded: the platform
4
4
  # can't hand a stored value back, and live keys don't belong on a laptop. Set
5
5
  # local stand-ins here; the worker reads them as c.env.NAME, exactly as it
6
6
  # reads the dashboard's in production. `canopy dev` prints which dashboard
@@ -56,7 +56,7 @@ the [Canopy dashboard](https://dashboard.thanarie.dev).
56
56
  - Do not add auth — the platform authenticates every request before it
57
57
  reaches this app or the Worker.
58
58
  - Do not store secrets in the repo or SPA. Set them in the Canopy dashboard
59
- (Settings → Secrets); the Worker reads them as `c.env.SECRET_NAME`
59
+ (`canopy secrets set NAME`, or Settings → Secrets); the Worker reads them as `c.env.SECRET_NAME`
60
60
  (Workers-shaped bindings — not `process.env`). Locally, put stand-in values
61
61
  in `.dev.vars` (gitignored, see `.dev.vars.example`) — dashboard values are
62
62
  never downloaded, and `canopy dev` prints which names are still missing. Platform access still goes
@@ -25,7 +25,7 @@ interface Env {
25
25
  // DB — this project's database: `dialect` plus
26
26
  // `query(text, params) => { rows, rowCount }`
27
27
  // BLOB — this project's files: `get` / `put` / `list` / `delete`
28
- // Dashboard secrets (Settings → Secrets) land as Workers-shaped bindings:
28
+ // Project secrets (`canopy secrets set` / Settings → Secrets) land as Workers-shaped bindings:
29
29
  // MY_API_KEY?: string;
30
30
  }
31
31
 
@@ -1,5 +1,5 @@
1
1
  import { a as projectDirArgs, c as resolveProject, o as projectFlagArgs } from "./site-DM2J10CB.mjs";
2
- import { l as apiRequest, r as reportErrors, t as formatDateTime } from "./ui-CUNkis89.mjs";
2
+ import { l as apiRequest, r as reportErrors, t as formatDateTime } from "./ui-DR-siu2S.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  const tokenCommand = defineCommand({
5
5
  meta: {
@@ -137,6 +137,26 @@ const ERROR_COPY = {
137
137
  worker_egress_sync_failed: {
138
138
  why: "the list was saved but redeploying the running worker with it failed",
139
139
  fix: "Retry, or redeploy the project (`canopy deploy`) to apply the new allowlist."
140
+ },
141
+ invalid_key: {
142
+ why: "secret names must be UPPER_SNAKE_CASE (a letter, then letters/digits/underscores, at most 64 chars)",
143
+ fix: "Rename it, e.g. STRIPE_SECRET."
144
+ },
145
+ reserved_key: {
146
+ why: "that name is reserved for the platform runtime",
147
+ fix: "Pick a name that isn't a Canopy or runtime binding."
148
+ },
149
+ value_too_large: {
150
+ why: "a secret value is limited to 8 KiB",
151
+ fix: "Store a reference (an id or a shorter key) and fetch the large payload from your worker instead."
152
+ },
153
+ too_many_secrets: {
154
+ why: "the project already holds the maximum number of secrets (64)",
155
+ fix: "Remove one you no longer need (`canopy secrets remove NAME`) first."
156
+ },
157
+ worker_secret_sync_failed: {
158
+ why: "the secret was saved but redeploying the running worker with it failed",
159
+ fix: "Retry, or redeploy the project (`canopy deploy`) to apply it."
140
160
  }
141
161
  };
142
162
  /** Generic explanation for a status code when we have no per-code copy. */
@@ -154,21 +174,40 @@ function statusFix(status) {
154
174
  if (status >= 500) return "This is usually temporary — wait a moment and try again.";
155
175
  return "Re-check the command arguments and try again.";
156
176
  }
177
+ /**
178
+ * One trace per CLI invocation (docs/42-observability.md §3.7): every request
179
+ * this process makes carries the same W3C traceparent, so the platform's logs
180
+ * for a `canopy deploy` line up under one id. The api echoes it back as
181
+ * `x-canopy-request-id`, which failures print for the user to quote.
182
+ */
183
+ const TRACE_ID = randomHex(16);
184
+ function randomHex(bytes) {
185
+ const buf = new Uint8Array(bytes);
186
+ crypto.getRandomValues(buf);
187
+ let out = "";
188
+ for (const b of buf) out += b.toString(16).padStart(2, "0");
189
+ return out;
190
+ }
191
+ function traceHeaders() {
192
+ return { traceparent: `00-${TRACE_ID}-${randomHex(8)}-01` };
193
+ }
157
194
  var ApiError = class extends Error {
158
195
  status;
159
196
  code;
160
197
  method;
161
198
  path;
162
199
  detail;
200
+ requestId;
163
201
  /** Command-specific copy, layered on by `withContext` (or `fail` for 401s). */
164
202
  context;
165
- constructor(status, code, method, path, detail) {
203
+ constructor(status, code, method, path, detail, requestId) {
166
204
  super(detail ? `${code}: ${detail}` : code);
167
205
  this.status = status;
168
206
  this.code = code;
169
207
  this.method = method;
170
208
  this.path = path;
171
209
  this.detail = detail;
210
+ this.requestId = requestId;
172
211
  this.name = "ApiError";
173
212
  }
174
213
  /**
@@ -179,10 +218,12 @@ var ApiError = class extends Error {
179
218
  const copy = ERROR_COPY[this.code];
180
219
  const headline = this.context?.action ? `could not ${this.context.action}` : `${this.method} ${this.path} failed`;
181
220
  const detail = this.detail ? `: ${this.detail}` : "";
221
+ const why = `${this.status} ${this.code}${detail} — ${copy?.why ?? statusWhy(this.status)}`;
222
+ const fix = this.context?.fixes?.[this.code] ?? copy?.fix ?? statusFix(this.status);
182
223
  return {
183
224
  headline,
184
- why: `${this.status} ${this.code}${detail} — ${copy?.why ?? statusWhy(this.status)}`,
185
- fix: this.context?.fixes?.[this.code] ?? copy?.fix ?? statusFix(this.status)
225
+ why: this.requestId ? `${why} (request id ${this.requestId})` : why,
226
+ fix
186
227
  };
187
228
  }
188
229
  };
@@ -221,7 +262,7 @@ function credentials() {
221
262
  async function fail(response, isDeployToken, method, path) {
222
263
  const payload = await response.json().catch(() => ({}));
223
264
  const code = payload.error ?? (response.status === 401 ? "unauthenticated" : `http_${response.status}`);
224
- const err = new ApiError(response.status, code, method, path, payload.detail);
265
+ const err = new ApiError(response.status, code, method, path, payload.detail, response.headers.get("x-canopy-request-id") ?? void 0);
225
266
  if (response.status === 401) err.context = {
226
267
  action: "authenticate with Canopy",
227
268
  fixes: { [code]: isDeployToken ? "The deploy token was rejected — it may have been revoked. Create a new one in the dashboard (Settings → Deploy tokens) or with `canopy token create`." : "Your session has expired — run `canopy login` again." }
@@ -234,6 +275,7 @@ async function apiRequest(method, path, body, rawContentType, headers) {
234
275
  const response = await fetch(`${apiUrl}/control/v1${path}`, {
235
276
  method,
236
277
  headers: {
278
+ ...traceHeaders(),
237
279
  authorization: `Bearer ${token}`,
238
280
  ...body === void 0 || isForm ? {} : { "content-type": rawContentType ?? "application/json" },
239
281
  ...headers
@@ -253,6 +295,7 @@ async function apiUpload(path, bytes, contentType) {
253
295
  const response = await fetch(`${apiUrl}/control/v1${path}`, {
254
296
  method: "POST",
255
297
  headers: {
298
+ ...traceHeaders(),
256
299
  authorization: `Bearer ${token}`,
257
300
  "content-type": contentType
258
301
  },
@@ -264,7 +307,10 @@ async function apiUpload(path, bytes, contentType) {
264
307
  /** Authenticated GET returning raw bytes (e.g. a deploy zip). */
265
308
  async function apiDownload(path) {
266
309
  const { apiUrl, token, isDeployToken } = credentials();
267
- const response = await fetch(`${apiUrl}/control/v1${path}`, { headers: { authorization: `Bearer ${token}` } });
310
+ const response = await fetch(`${apiUrl}/control/v1${path}`, { headers: {
311
+ ...traceHeaders(),
312
+ authorization: `Bearer ${token}`
313
+ } });
268
314
  if (!response.ok) return fail(response, isDeployToken, "GET", path);
269
315
  return Buffer.from(await response.arrayBuffer());
270
316
  }
@@ -278,6 +324,7 @@ async function apiDataRequest(method, path, projectSlug, body, rawContentType, c
278
324
  const response = await fetch(`${apiUrl}/data/v1${path}`, {
279
325
  method,
280
326
  headers: {
327
+ ...traceHeaders(),
281
328
  authorization: `Bearer ${token}`,
282
329
  "x-canopy-project": projectSlug,
283
330
  ...channel === "staging" ? { "x-canopy-channel": "staging" } : {},
@@ -1,5 +1,5 @@
1
1
  import { t as PACKAGE_NAME } from "./version-check-CcsA1QjI.mjs";
2
- import { r as reportErrors } from "./ui-CUNkis89.mjs";
2
+ import { r as reportErrors } from "./ui-DR-siu2S.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import { spawn } from "node:child_process";
5
5
  import { fileURLToPath } from "node:url";
@@ -112,7 +112,7 @@ const upgradeCommand = defineCommand({
112
112
  const spec = `${PACKAGE_NAME}@${args.version ?? "latest"}`;
113
113
  const command = pmCommand(pm);
114
114
  const cmd = installArgs(pm, spec);
115
- const { skillInstalled } = await import("./skill-PoaZrCNy.mjs");
115
+ const { skillInstalled } = await import("./skill-DmVlZXgv.mjs");
116
116
  const { homedir } = await import("node:os");
117
117
  const refreshGlobal = skillInstalled(homedir());
118
118
  const refreshRepo = process.cwd() !== homedir() && skillInstalled(process.cwd());
@@ -1,5 +1,5 @@
1
1
  import { c as resolveProject, o as projectFlagArgs } from "./site-DM2J10CB.mjs";
2
- import { d as withContext, l as apiRequest, r as reportErrors } from "./ui-CUNkis89.mjs";
2
+ import { d as withContext, l as apiRequest, r as reportErrors } from "./ui-DR-siu2S.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  //#region src/usage.ts
5
5
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mediamonks/canopy-cli",
3
- "version": "0.7.25",
3
+ "version": "0.7.26",
4
4
  "description": "Canopy CLI — create, develop and deploy Canopy projects.",
5
5
  "bin": {
6
6
  "canopy": "./dist/index.mjs"
@@ -1,2 +0,0 @@
1
- import { t as devCommand } from "./dev-BanjQNiD.mjs";
2
- export { devCommand };