@mediamonks/canopy-cli 0.7.41 → 0.7.43

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 (47) hide show
  1. package/dist/{ai-PXXMv7kO.mjs → ai-FBHTSCLZ.mjs} +2 -2
  2. package/dist/{api-BHzCvDcY.mjs → api-Bl6SG8Oq.mjs} +63 -5
  3. package/dist/{browse-DRz1DowL.mjs → browse-dyOThTyf.mjs} +2 -2
  4. package/dist/{db-5LfBG6N7.mjs → db-BJJGydgL.mjs} +4 -4
  5. package/dist/{deploy-6rYq1Iq8.mjs → deploy-ExrIeX_m.mjs} +27 -8
  6. package/dist/dev-BmI5kwoG.mjs +2 -0
  7. package/dist/{dev-D_gyIeUq.mjs → dev-adLLSY1F.mjs} +8 -8
  8. package/dist/{dev-worker-D78leKBJ.mjs → dev-worker-DJekKSdb.mjs} +1 -1
  9. package/dist/docs/ci.md +19 -1
  10. package/dist/docs/cli-rename.md +51 -0
  11. package/dist/docs/developers-architecture.md +12 -0
  12. package/dist/docs/git.md +5 -0
  13. package/dist/docs/index.json +11 -1
  14. package/dist/docs/platform-egress.md +14 -7
  15. package/dist/docs/rename.md +65 -0
  16. package/dist/docs/static-serving.md +13 -4
  17. package/dist/{docs-B9xGnLSR.mjs → docs-CaAgJWK3.mjs} +4 -4
  18. package/dist/{egress-CCpOsYpS.mjs → egress-C2amvqmE.mjs} +3 -3
  19. package/dist/{feedback-CdjtPumG.mjs → feedback-CMnG2Zvs.mjs} +4 -4
  20. package/dist/index.mjs +33 -32
  21. package/dist/{local-Dshy0NsJ.mjs → local-2xtxtoUd.mjs} +3 -3
  22. package/dist/{login-CKNFCp7c.mjs → login-CREcZfLH.mjs} +3 -3
  23. package/dist/{logs-D3WakFME.mjs → logs-BnrNZvLc.mjs} +3 -3
  24. package/dist/{mcp-CoVS5Mj3.mjs → mcp-BVIU4j9N.mjs} +8 -6
  25. package/dist/{migrations-DDJ5JJqj.mjs → migrations-D8NqjC5I.mjs} +2 -2
  26. package/dist/{open-BPYozPQo.mjs → open-Bpz4C2Sr.mjs} +2 -2
  27. package/dist/{package-SWgTmctH.mjs → package-XOtqboS1.mjs} +1 -1
  28. package/dist/project-template-715H_lnu.mjs +2 -0
  29. package/dist/{project-template-DACdMdpn.mjs → project-template-Cdr1uwwE.mjs} +3 -3
  30. package/dist/{projects-naWQ89Qj.mjs → projects-BRG2HnBs.mjs} +5 -5
  31. package/dist/rename-BGKa25Rq.mjs +81 -0
  32. package/dist/{repo-BCua4L-I.mjs → repo-CwT2RDoW.mjs} +5 -3
  33. package/dist/repo-DmFs6H9m.mjs +2 -0
  34. package/dist/{schedule-DjHcaxyt.mjs → schedule-BcqBspn3.mjs} +3 -3
  35. package/dist/{secrets-COwRvCev.mjs → secrets-BjbIi7Gp.mjs} +3 -3
  36. package/dist/{site-DM2J10CB.mjs → site-0i7wAJqg.mjs} +24 -1
  37. package/dist/{skill-DSYQA4Ls.mjs → skill-Ct-aCOzg.mjs} +2 -2
  38. package/dist/{src-RUf3Gfuh.mjs → src-CScXotBB.mjs} +727 -1
  39. package/dist/{staging-CYMTZyyM.mjs → staging-_2Js0Fkc.mjs} +3 -3
  40. package/dist/{token-CCpJ1ES4.mjs → token-7vSNuz-X.mjs} +3 -3
  41. package/dist/{ui-SBUdC9rX.mjs → ui-CLnICOne.mjs} +1 -1
  42. package/dist/{upgrade-B_tLCpc4.mjs → upgrade-DJogeXAF.mjs} +2 -2
  43. package/dist/{usage-BSPnsMXd.mjs → usage-DGGmTwjJ.mjs} +3 -3
  44. package/package.json +1 -1
  45. package/dist/dev-D38FQ2LD.mjs +0 -2
  46. package/dist/project-template-CMgD5hnV.mjs +0 -2
  47. package/dist/repo-Un_p617X.mjs +0 -2
@@ -1,5 +1,5 @@
1
- import { a as apiRequest, l as withContext } from "./api-BHzCvDcY.mjs";
2
- import { r as reportErrors } from "./ui-SBUdC9rX.mjs";
1
+ import { a as apiRequest, f as withContext } from "./api-Bl6SG8Oq.mjs";
2
+ import { r as reportErrors } from "./ui-CLnICOne.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  //#region src/ai.ts
5
5
  /**
@@ -1,4 +1,4 @@
1
- import { n as siteConfig } from "./site-DM2J10CB.mjs";
1
+ import { r as siteConfig, t as projectDir, u as rewriteCanopyJsonSlug } from "./site-0i7wAJqg.mjs";
2
2
  //#region src/api.ts
3
3
  /**
4
4
  * Authenticated requests to the Canopy control plane. Credentials, in order:
@@ -29,7 +29,7 @@ const ERROR_COPY = {
29
29
  fix: "Choose a different slug."
30
30
  },
31
31
  invalid_slug: {
32
- why: "the slug is not a valid format",
32
+ why: "the slug breaks one of the naming rules",
33
33
  fix: "Use lowercase letters, digits and hyphens only (e.g. my-project)."
34
34
  },
35
35
  project_paused: {
@@ -57,11 +57,11 @@ const ERROR_COPY = {
57
57
  fix: "Per-project workers, schedules and secrets are capability-gated per target — remove the unsupported part (e.g. the worker bundle) or run against a target that supports it."
58
58
  },
59
59
  deploy_too_large: {
60
- why: "the deploy bundle exceeds the 25 MB limit",
60
+ why: "the deploy bundle exceeds the 50 MB limit",
61
61
  fix: "Trim large assets from the build output (or move them to blob storage), then redeploy."
62
62
  },
63
63
  too_many_files: {
64
- why: "the deploy has more than 500 files",
64
+ why: "the deploy has more than 1000 files",
65
65
  fix: "Reduce the file count — bundle/minify assets or drop what isn't served — then redeploy."
66
66
  },
67
67
  empty_deploy: {
@@ -191,6 +191,26 @@ const ERROR_COPY = {
191
191
  worker_secret_sync_failed: {
192
192
  why: "the secret was saved but redeploying the running worker with it failed",
193
193
  fix: "Retry, or redeploy the project (`canopy deploy`) to apply it."
194
+ },
195
+ slug_unchanged: {
196
+ why: "the project already has that slug",
197
+ fix: "Pass the new slug you want, e.g. `canopy rename my-new-name`."
198
+ },
199
+ rename_not_allowed: {
200
+ why: "Canopy's own projects (dashboard, www, docs) keep their names",
201
+ fix: "Nothing to do — those addresses are fixed."
202
+ },
203
+ rename_too_soon: {
204
+ why: "a project can be renamed once a day",
205
+ fix: "Try again after the time shown, or ask a Canopy admin if it can't wait."
206
+ },
207
+ too_many_old_slugs: {
208
+ why: "a project keeps at most 5 old slugs, and each one stays reserved for it",
209
+ fix: "Rename back to one of its old slugs instead (`canopy projects` shows them in the dashboard's Settings), or ask a Canopy admin."
210
+ },
211
+ rename_conflict: {
212
+ why: "the project changed while the rename ran",
213
+ fix: "Run the command again."
194
214
  }
195
215
  };
196
216
  /** Generic explanation for a status code when we have no per-code copy. */
@@ -260,6 +280,40 @@ function commonHeaders() {
260
280
  ...agentHeaders()
261
281
  };
262
282
  }
283
+ /**
284
+ * Renamed projects (OpenSpec project-slug-rename). A request that names an
285
+ * old slug still reaches the project, and the answer carries its current
286
+ * slug in `Canopy-Project-Slug`. By default the CLI then points canopy.json
287
+ * at it (when that file named the old slug) and says so once; `canopy mcp
288
+ * serve` and the git credential helper only say so, because what names the
289
+ * slug there is the user's own configuration, not ours to edit.
290
+ */
291
+ const RENAMED_HEADER = "canopy-project-slug";
292
+ const renames = /* @__PURE__ */ new Map();
293
+ let renameHandling = "rewrite";
294
+ /** Report renames without editing files (mcp serve, the git credential helper). */
295
+ function noticeRenamesOnly() {
296
+ renameHandling = "notice";
297
+ }
298
+ /** The current slug of a project this process saw renamed; `slug` when it saw none. */
299
+ function currentSlug(slug) {
300
+ return renames.get(slug) ?? slug;
301
+ }
302
+ /** Act on a response that says `named` is an old slug. Exported for the credential helper. */
303
+ function followRename(response, named) {
304
+ const current = response.headers.get(RENAMED_HEADER);
305
+ if (!named || !current || current === named || renames.has(named)) return;
306
+ renames.set(named, current);
307
+ if (renameHandling === "rewrite" && rewriteCanopyJsonSlug(projectDir(), named, current)) {
308
+ console.error(`canopy: project "${named}" is now "${current}" — updated canopy.json.`);
309
+ return;
310
+ }
311
+ console.error(`canopy: project "${named}" is now "${current}" — use "${current}" where you name it (MCP client config, scripts); the old name keeps working for now.`);
312
+ }
313
+ /** The slug a control-plane path names (`/projects/<slug>/…`), if any. */
314
+ function slugInPath(path) {
315
+ return /^\/projects\/([^/?#]+)/.exec(path)?.[1];
316
+ }
263
317
  var ApiError = class extends Error {
264
318
  status;
265
319
  code;
@@ -351,6 +405,7 @@ async function apiRequest(method, path, body, rawContentType, headers) {
351
405
  },
352
406
  body: body === void 0 ? void 0 : isForm ? body : rawContentType !== void 0 ? body : JSON.stringify(body)
353
407
  });
408
+ followRename(response, slugInPath(path));
354
409
  if (!response.ok) return fail(response, isDeployToken, method, path);
355
410
  if (response.status === 204) return void 0;
356
411
  return await response.json();
@@ -370,6 +425,7 @@ async function apiUpload(path, bytes, contentType) {
370
425
  },
371
426
  body: bytes
372
427
  });
428
+ followRename(response, slugInPath(path));
373
429
  if (!response.ok) return fail(response, isDeployToken, "POST", path);
374
430
  return await response.json();
375
431
  }
@@ -380,6 +436,7 @@ async function apiDownload(path) {
380
436
  ...commonHeaders(),
381
437
  authorization: `Bearer ${token}`
382
438
  } });
439
+ followRename(response, slugInPath(path));
383
440
  if (!response.ok) return fail(response, isDeployToken, "GET", path);
384
441
  return Buffer.from(await response.arrayBuffer());
385
442
  }
@@ -401,9 +458,10 @@ async function apiDataRequest(method, path, projectSlug, body, rawContentType, c
401
458
  },
402
459
  body: body === void 0 ? void 0 : rawContentType !== void 0 ? body : JSON.stringify(body)
403
460
  });
461
+ followRename(response, projectSlug);
404
462
  if (!response.ok) return fail(response, isDeployToken, method, path);
405
463
  if (response.status === 204) return void 0;
406
464
  return await response.json();
407
465
  }
408
466
  //#endregion
409
- export { apiRequest as a, credentials as c, apiDownload as i, withContext as l, agentHeaders as n, apiUpload as o, apiDataRequest as r, commonHeaders as s, ApiError as t };
467
+ export { apiRequest as a, credentials as c, noticeRenamesOnly as d, withContext as f, apiDownload as i, currentSlug as l, agentHeaders as n, apiUpload as o, apiDataRequest as r, commonHeaders as s, ApiError as t, followRename as u };
@@ -1,5 +1,5 @@
1
- import { d as tryResolveProject, n as siteConfig } from "./site-DM2J10CB.mjs";
2
- import { a as apiRequest, t as ApiError } from "./api-BHzCvDcY.mjs";
1
+ import { p as tryResolveProject, r as siteConfig } from "./site-0i7wAJqg.mjs";
2
+ import { a as apiRequest, t as ApiError } from "./api-Bl6SG8Oq.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  //#region src/browse.ts
5
5
  /**
@@ -1,7 +1,7 @@
1
- import { c as resolveProject, o as projectFlagArgs } from "./site-DM2J10CB.mjs";
2
- import { a as apiRequest, l as withContext, r as apiDataRequest } from "./api-BHzCvDcY.mjs";
3
- import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-SBUdC9rX.mjs";
4
- import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-DDJ5JJqj.mjs";
1
+ import { l as resolveProject, s as projectFlagArgs } from "./site-0i7wAJqg.mjs";
2
+ import { a as apiRequest, f as withContext, r as apiDataRequest } from "./api-Bl6SG8Oq.mjs";
3
+ import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-CLnICOne.mjs";
4
+ import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-D8NqjC5I.mjs";
5
5
  import { defineCommand } from "citty";
6
6
  import { readFileSync, writeFileSync } from "node:fs";
7
7
  import * as p from "@clack/prompts";
@@ -1,7 +1,7 @@
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 apiRequest, i as apiDownload, l as withContext, r as apiDataRequest } from "./api-BHzCvDcY.mjs";
3
- import { a as unwrap, r as reportErrors, t as formatDateTime } from "./ui-SBUdC9rX.mjs";
4
- import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-DDJ5JJqj.mjs";
1
+ import { c as projectUrl, d as stagingUrl, f as tryReadCanopyJson, l as resolveProject, o as projectDirArgs, r as siteConfig, s as projectFlagArgs } from "./site-0i7wAJqg.mjs";
2
+ import { a as apiRequest, f as withContext, i as apiDownload, l as currentSlug, r as apiDataRequest } from "./api-Bl6SG8Oq.mjs";
3
+ import { a as unwrap, r as reportErrors, t as formatDateTime } from "./ui-CLnICOne.mjs";
4
+ import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-D8NqjC5I.mjs";
5
5
  import { builtinModules } from "node:module";
6
6
  import { defineCommand } from "citty";
7
7
  import { mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
@@ -290,6 +290,11 @@ const deployCommand = defineCommand({
290
290
  channel
291
291
  }, void 0, source), { action: `deploy "${slug}"` });
292
292
  stagingCreated = upload.stagingCreated === true;
293
+ const overLimit = bundleLimitError(files, upload);
294
+ if (overLimit) {
295
+ spin.error("Deploy refused — nothing was uploaded.");
296
+ throw new Error(overLimit);
297
+ }
293
298
  const uploads = [...files, ...workerBytes ? [{
294
299
  path: WORKER_BUNDLE_PATH,
295
300
  data: workerBytes
@@ -314,10 +319,10 @@ const deployCommand = defineCommand({
314
319
  }
315
320
  const backend = result.worker?.serviceUrl ? ` · backend ${result.worker.serviceUrl}` : "";
316
321
  if (channel === "staging") {
317
- spin.stop(`Staging: ${stagingUrl(slug)} (v${result.version})${backend}`);
322
+ spin.stop(`Staging: ${stagingUrl(currentSlug(slug))} (v${result.version})${backend}`);
318
323
  if (stagingCreated) console.log(" staging channel created — database cloned from live");
319
324
  console.log(` happy with it? \`canopy promote\` makes v${result.version} live`);
320
- } else spin.stop(`Live: ${projectUrl(slug)} (v${result.version})${backend}`);
325
+ } else spin.stop(`Live: ${projectUrl(currentSlug(slug))} (v${result.version})${backend}`);
321
326
  if (migrateResult !== void 0) for (const line of describeMigrateResult(migrateResult)) console.log(` ${line}`);
322
327
  })
323
328
  });
@@ -371,12 +376,26 @@ const promoteCommand = defineCommand({
371
376
  }
372
377
  const backend = result.worker?.serviceUrl ? ` · backend ${result.worker.serviceUrl}` : "";
373
378
  const was = result.previous === null ? "" : `, was v${result.previous}`;
374
- spin.stop(`Live: ${projectUrl(slug)} (v${result.version}${was})${backend}`);
379
+ spin.stop(`Live: ${projectUrl(currentSlug(slug))} (v${result.version}${was})${backend}`);
375
380
  for (const name of result.migrationsApplied) console.log(` applied ${name}`);
376
381
  if (result.snapshot !== void 0) console.log(` pre-promote snapshot: ${result.snapshot} (canopy db restore --snapshot …)`);
377
382
  })
378
383
  });
379
384
  const UPLOAD_CONCURRENCY = 8;
385
+ /**
386
+ * Why the asset files exceed the limits the platform stated, or null. The
387
+ * worker bundle has its own key and isn't counted, as on the server. A server
388
+ * that states no limits isn't checked here; completion still enforces them.
389
+ */
390
+ function bundleLimitError(files, limits) {
391
+ if (limits.maxFiles !== void 0 && files.length > limits.maxFiles) return `This deploy has ${files.length} files; the limit is ${limits.maxFiles}. Bundle/minify assets or drop what isn't served, then redeploy.`;
392
+ const bytes = files.reduce((sum, f) => sum + f.data.byteLength, 0);
393
+ if (limits.maxTotalBytes !== void 0 && bytes > limits.maxTotalBytes) {
394
+ const mb = (n) => `${(n / 1024 / 1024).toFixed(1)} MB`;
395
+ return `This deploy is ${mb(bytes)}; the limit is ${mb(limits.maxTotalBytes)}. Move large images or fonts to file storage, then redeploy.`;
396
+ }
397
+ return null;
398
+ }
380
399
  /** PUT each file to /data/v1/uploads/<grant> — the grant is the credential. */
381
400
  async function uploadFiles(grant, files) {
382
401
  const { apiUrl } = siteConfig();
@@ -445,7 +464,7 @@ const rollbackCommand = defineCommand({
445
464
  console.log(`Rolling back ${slug} to v${target}`);
446
465
  const { live: result } = await apiRequest("POST", `/projects/${slug}/deploys/${target}/rollback`);
447
466
  const worker = result.worker ? ` · worker v${result.worker.version}` : "";
448
- console.log(`Live: ${projectUrl(slug)} (v${result.version}, was v${result.previous ?? "?"})${worker}`);
467
+ console.log(`Live: ${projectUrl(currentSlug(slug))} (v${result.version}, was v${result.previous ?? "?"})${worker}`);
449
468
  })
450
469
  });
451
470
  const downloadCommand = defineCommand({
@@ -0,0 +1,2 @@
1
+ import { n as devCommand } from "./dev-adLLSY1F.mjs";
2
+ export { devCommand };
@@ -1,8 +1,8 @@
1
- import { a as projectDirArgs, c as resolveProject, i as projectDialect, n as siteConfig, u as tryReadCanopyJson } from "./site-DM2J10CB.mjs";
2
- import { a as apiRequest, l as withContext, t as ApiError } from "./api-BHzCvDcY.mjs";
3
- import { r as reportErrors, t as formatDateTime } from "./ui-SBUdC9rX.mjs";
4
- import { r as readMigrationFiles } from "./migrations-DDJ5JJqj.mjs";
5
- import { i as stampWorkerHeaders, n as WORKER_ATTEST_HEADER } from "./src-RUf3Gfuh.mjs";
1
+ import { a as projectDialect, f as tryReadCanopyJson, l as resolveProject, o as projectDirArgs, r as siteConfig } from "./site-0i7wAJqg.mjs";
2
+ import { a as apiRequest, f as withContext, t as ApiError } from "./api-Bl6SG8Oq.mjs";
3
+ import { r as reportErrors, t as formatDateTime } from "./ui-CLnICOne.mjs";
4
+ import { r as readMigrationFiles } from "./migrations-D8NqjC5I.mjs";
5
+ import { i as stampWorkerHeaders, n as WORKER_ATTEST_HEADER } from "./src-CScXotBB.mjs";
6
6
  import { n as writeDevServerRecord } from "./dev-server-file-Dk9pk4AR.mjs";
7
7
  import { createRequire } from "node:module";
8
8
  import { defineCommand } from "citty";
@@ -41,7 +41,7 @@ async function workerPlugin(flags, slug, workerAttest, remoteUser) {
41
41
  console.log(`note: canopy.json points "worker" at ${entry}, which does not exist — skipping.`);
42
42
  return;
43
43
  }
44
- const { missingSecretNames, readDevVars, workerDevPlugin } = await import("./dev-worker-D78leKBJ.mjs");
44
+ const { missingSecretNames, readDevVars, workerDevPlugin } = await import("./dev-worker-DJekKSdb.mjs");
45
45
  const secrets = readDevVars(flags.dir);
46
46
  const email = siteConfig().email;
47
47
  const user = await remoteUser?.() ?? (email ? {
@@ -251,7 +251,7 @@ async function devRemote(flags, slug) {
251
251
  async function devLocal(flags, slug) {
252
252
  let dump;
253
253
  if (flags.pull) {
254
- const { fetchProjectDump } = await import("./local-Dshy0NsJ.mjs");
254
+ const { fetchProjectDump } = await import("./local-2xtxtoUd.mjs");
255
255
  console.log(`Pulling a fresh export of ${slug}${flags.staging ? " [staging]" : ""}…`);
256
256
  dump = await fetchProjectDump(slug, flags.staging ? "staging" : "live");
257
257
  }
@@ -267,7 +267,7 @@ async function devLocal(flags, slug) {
267
267
  console.log(`Note: ai/services will be unavailable — could not reach the platform (${reason}).`);
268
268
  }
269
269
  const workerAttest = randomBytes(24).toString("base64url");
270
- const { createSandbox } = await import("./local-Dshy0NsJ.mjs");
270
+ const { createSandbox } = await import("./local-2xtxtoUd.mjs");
271
271
  const sandbox = await createSandbox({
272
272
  slug,
273
273
  persistDir: join(flags.dir, ".canopy", "local"),
@@ -1,4 +1,4 @@
1
- import { i as stampWorkerHeaders, n as WORKER_ATTEST_HEADER, r as redactCanopyCredentials, t as CALLER_HEADER } from "./src-RUf3Gfuh.mjs";
1
+ import { i as stampWorkerHeaders, n as WORKER_ATTEST_HEADER, r as redactCanopyCredentials, t as CALLER_HEADER } from "./src-CScXotBB.mjs";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { relative, resolve, sep } from "node:path";
4
4
  //#region src/dev-worker.ts
package/dist/docs/ci.md CHANGED
@@ -1,8 +1,26 @@
1
1
  ---
2
2
  title: CI and deploy tokens
3
- description: Push-to-main deploys using a project-scoped deploy token.
3
+ description: Push-to-main deploys using a project-scoped deploy token, and why we recommend deploying from your own machine instead.
4
4
  ---
5
5
 
6
+ ## Our recommendation: deploy manually
7
+
8
+ Most Canopy users don't have a GitHub Enterprise seat, so they can't see or
9
+ interact with GitHub Actions. That is why Canopy repositories are created with
10
+ Actions turned **off**: pushing a workflow file works, but nothing runs, and
11
+ there is no Actions tab for you to look at. Turning Actions on for a repository
12
+ is something only a GitHub administrator can do, and you would still not see
13
+ the runs.
14
+
15
+ Deploy from your machine (or let your coding agent do it): run
16
+ `canopy deploy --staging`, check the result, then `canopy promote`. It is
17
+ usually quicker than waiting for a pipeline, and your agent can verify the
18
+ deploy right afterwards. Run your typecheck, lint and tests locally before you
19
+ deploy.
20
+
21
+ The rest of this page is for teams that run their own CI somewhere they can
22
+ see it.
23
+
6
24
  Interactive `canopy login` is for humans at a terminal. For automated deploys
7
25
  (GitHub Actions, etc.) use a **deploy token**: a project-scoped credential that
8
26
  can deploy that one project and nothing else.
@@ -0,0 +1,51 @@
1
+ ---
2
+ title: canopy rename
3
+ description: Change a project's slug — its address — and update canopy.json.
4
+ ---
5
+
6
+ `canopy rename` gives a project a new slug, and with it a new address. The old
7
+ slug stays with the project: old links redirect, and everything that still
8
+ names it keeps working. See [Rename a project](https://docs.canopy.website/guides/rename) for what changes
9
+ and what doesn't.
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ canopy rename my-new-name # in the project's folder
15
+ canopy rename my-new-name --project old-name # name the project yourself
16
+ canopy rename my-new-name --yes # skip the confirmation
17
+ ```
18
+
19
+ ## Arguments & flags
20
+
21
+ - `<slug>` — the new slug. Same rules as `canopy create`: 2–40 lowercase letters,
22
+ digits and dashes, starting with a letter.
23
+ - `--project <slug>` — the project to rename. Defaults to the `slug` in
24
+ `canopy.json`.
25
+ - `[dir]` — the project folder (defaults to the current directory). Its
26
+ `canopy.json` is updated after the rename.
27
+ - `--yes` — rename without asking. Required on non-interactive runs.
28
+ - `--json` — print the result as JSON: the project, its old slugs, and whether
29
+ the backend refresh and Slack app update are still to come.
30
+
31
+ ## Details
32
+
33
+ Before renaming, the CLI lists what changes for people: the address, the email
34
+ sender, client-password visitors signing in again, and visitors' browser storage
35
+ starting empty. Only the owner or a Canopy admin can rename. A project can be
36
+ renamed once a day and keeps at most 5 old slugs; renaming back to one of them
37
+ is always allowed.
38
+
39
+ Other commands notice a rename too. When `canopy.json` still names the old
40
+ slug, the next command that talks to the project updates it and says so in one
41
+ line. `canopy mcp serve` and the git credential helper only print a notice,
42
+ since they're started from your own configuration.
43
+
44
+ ## Examples
45
+
46
+ ```bash
47
+ canopy rename q4-dashboard
48
+ ```
49
+
50
+ Shows what changes, asks to confirm, renames, and points `canopy.json` at
51
+ `q4-dashboard`.
@@ -103,6 +103,18 @@ else is still static serving. A few things to know:
103
103
  `process.env`); dashboard Schedules invoke an exported
104
104
  `scheduled(event, env)` — switch on `event.name`.
105
105
 
106
+ - **Backends run as Workers, on every site.** Your worker runs in its own
107
+ isolated runtime, with the rules Workers have everywhere:
108
+ - no file system to write to, and no child processes;
109
+ - `eval` and `new Function` don't work;
110
+ - a connection or stream belongs to the request that opened it;
111
+ - work you start but don't await stops when the response is sent. Hand it to
112
+ `c.executionCtx.waitUntil(...)` if it has to finish.
113
+
114
+ Node built-ins from the `nodejs_compat` set work (`node:crypto`,
115
+ `node:buffer`, `node:https`, …). `process.env` holds your secrets and the
116
+ project's own names, never the platform's.
117
+
106
118
  > **Note:** Backends run on the `main` and `gcp-pilot` sites. On a site without backend support, a deploy that
107
119
  > includes one answers with a clear "not supported here" rather than failing obscurely;
108
120
  > frontend-only projects deploy everywhere.
package/dist/docs/git.md CHANGED
@@ -79,6 +79,11 @@ sign in to a Canopy repository; use the terminal for those.
79
79
  - **Deleting is for good.** `canopy repo delete` (owner only) removes the
80
80
  repository and all its history. Deleting the project deletes its repository
81
81
  too.
82
+ - **No CI on the repository.** GitHub Actions are turned off on Canopy
83
+ repositories, so workflow files you push don't run, and you can't see
84
+ Actions runs without a GitHub seat. Run your checks locally and deploy with
85
+ `canopy deploy --staging` then `canopy promote` (see
86
+ [CI and deploy tokens](https://docs.canopy.website/guides/ci)).
82
87
  - **Moving to your own GitHub later** is ordinary git: create an empty
83
88
  repository there, then `git remote add github <its url>` and
84
89
  `git push github --all`.
@@ -65,7 +65,7 @@
65
65
  {
66
66
  "slug": "ci",
67
67
  "title": "CI and deploy tokens",
68
- "summary": "Push-to-main deploys using a project-scoped deploy token."
68
+ "summary": "Push-to-main deploys using a project-scoped deploy token, and why we recommend deploying from your own machine instead."
69
69
  },
70
70
  {
71
71
  "slug": "git",
@@ -77,6 +77,11 @@
77
77
  "title": "Start from a template",
78
78
  "summary": "Begin a project from someone else's working one — a copy of their code and setup, never their data."
79
79
  },
80
+ {
81
+ "slug": "rename",
82
+ "title": "Rename a project",
83
+ "summary": "Change a project's slug — its address — without breaking links, checkouts or integrations."
84
+ },
80
85
  {
81
86
  "slug": "agent-tools",
82
87
  "title": "Agent tools (MCP)",
@@ -231,6 +236,11 @@
231
236
  "title": "canopy delete",
232
237
  "summary": "Permanently delete a Canopy project — its deploys, database, and files."
233
238
  },
239
+ {
240
+ "slug": "cli-rename",
241
+ "title": "canopy rename",
242
+ "summary": "Change a project's slug — its address — and update canopy.json."
243
+ },
234
244
  {
235
245
  "slug": "cli-dev",
236
246
  "title": "canopy dev",
@@ -3,11 +3,11 @@ title: Outbound requests
3
3
  description: Which external hosts your project's backend may fetch — a per-project allowlist you manage yourself, or open mode for apps that fetch arbitrary URLs.
4
4
  ---
5
5
 
6
- Outbound `fetch` from a project's backend worker is **deny-by-default**: the
7
- platform only lets requests through to platform infrastructure (your database,
8
- files, AI, the Canopy API) and to the hosts on your project's own
9
- **egress allowlist**. Anything else is answered locally with a 403
10
- `egress_denied` — the request never leaves the runtime.
6
+ Outbound requests from a project's backend worker are **deny-by-default**,
7
+ whatever the code uses to make them: `fetch`, `node:https`, or a raw socket.
8
+ The platform only lets them through to platform infrastructure (your
9
+ database, files, AI, the Canopy API) and to the hosts on your project's own
10
+ **egress allowlist**. Anything else is stopped before it leaves the runtime.
11
11
 
12
12
  That default is deliberate: a thousand idle vibecoded projects should not be
13
13
  able to call arbitrary internet hosts. When your project genuinely needs an
@@ -48,8 +48,8 @@ punycode form. A project can list up to **32** hosts.
48
48
 
49
49
  Everything you reach through the SDK — `canopy.db`, `canopy.blob`,
50
50
  `canopy.ai`, and `env.CANOPY.fetch` — goes through the platform and is always
51
- allowed. The allowlist only governs direct `fetch` calls from your worker code
52
- to the outside world. (Frontend code in the browser is unaffected — this is
51
+ allowed. The allowlist governs every connection your worker code opens to the
52
+ outside world. (Frontend code in the browser is unaffected — this is
53
53
  about the worker.)
54
54
 
55
55
  ## When a request is denied
@@ -64,6 +64,13 @@ body:
64
64
  Allow the host and retry — see the
65
65
  [troubleshooting entry](https://docs.canopy.website/guides/troubleshooting#egress_denied-403).
66
66
 
67
+ That answer is for `fetch` and the libraries built on it. Other clients get a
68
+ plain connection failure instead: a `node:http` or `node:https` request, or a
69
+ raw TCP socket (`connect()` from `cloudflare:sockets`, `node:net`). Raw sockets
70
+ to allowed hosts work on sites that support them and fail on the rest. A host
71
+ on your list that resolves to a private or internal address is refused too,
72
+ so an allowed name can't lead your worker into the platform's own network.
73
+
67
74
  ## Recently blocked hosts
68
75
 
69
76
  You don't have to guess what got denied: the runtime reports every blocked
@@ -0,0 +1,65 @@
1
+ ---
2
+ title: Rename a project
3
+ description: Change a project's slug — its address — without breaking links, checkouts or integrations.
4
+ ---
5
+
6
+ A project's **slug** is its address: `<slug>.canopy.website`. You can change it
7
+ from the dashboard or the CLI. Nothing needs to be redeployed, and everything
8
+ that still uses the old slug keeps reaching the project.
9
+
10
+ ## How to rename
11
+
12
+ In the dashboard, open the project's **Settings** and use **Rename address**
13
+ on the _Project address_ card. Type the new slug, read what changes, and type
14
+ it again to confirm.
15
+
16
+ From the CLI, run it inside the project's folder:
17
+
18
+ ```bash
19
+ canopy rename my-new-name
20
+ ```
21
+
22
+ The CLI shows what changes and asks before it renames. It also updates
23
+ `canopy.json` in that folder. See [`canopy rename`](https://docs.canopy.website/cli/rename) for the flags.
24
+
25
+ Only the project's **owner** (or a Canopy admin) can rename it.
26
+
27
+ ## What changes
28
+
29
+ - **The address.** The project is now at `my-new-name.canopy.website`. The old
30
+ address sends visitors there, keeping the rest of the link
31
+ (`old-name.canopy.website/reports/42` opens `my-new-name.canopy.website/reports/42`).
32
+ The same goes for `staging-dot-` and pinned `v12-dot-` addresses.
33
+ - **The email sender.** Mail your project sends comes from `my-new-name@` instead
34
+ of `old-name@`.
35
+ - **Client-password visitors sign in again** on the new address. Their sign-in
36
+ belonged to the old one.
37
+ - **Browser storage starts empty** on the new address. Anything your app keeps
38
+ in the visitor's browser (localStorage, IndexedDB) stays with the old one.
39
+ - **New report keys** use the new slug: `my-new-name-18`.
40
+
41
+ ## What keeps working
42
+
43
+ The old slug stays with your project. Nobody else can create a project with it.
44
+
45
+ - Old links and bookmarks redirect, as above. Pages that were already open keep
46
+ working until they're reloaded.
47
+ - `canopy.json` files, CI deploy tokens, scripts and MCP client configs that
48
+ still say the old slug keep working. The CLI updates a `canopy.json` it finds
49
+ with the old slug, and tells you when anything else should be updated.
50
+ - Your backend keeps running, and the rename updates the `PROJECT_SLUG` it
51
+ sees. If that update fails, Settings says so and has a retry button; Canopy
52
+ also retries on its own, and the next deploy updates it too.
53
+ - Report keys with the old slug (`old-name-17`) still open the report.
54
+ - Your Slack app keeps receiving events and slash commands.
55
+ - `git push` and `git pull` keep working. The repository itself keeps its name.
56
+ - Database, files, deploys, schedules, secrets and members don't change.
57
+
58
+ ## Limits
59
+
60
+ - A project can be renamed **once a day**.
61
+ - A project keeps at most **5 old slugs**. Renaming back to one of them is always
62
+ allowed, and doesn't count as a new one.
63
+ - The new slug follows the same rules as a new project: 2–40 lowercase letters,
64
+ digits and dashes, starting with a letter.
65
+ - Canopy's own projects (`dashboard`, `www`, `docs`) can't be renamed.
@@ -22,8 +22,16 @@ in [members and access](https://docs.canopy.website/platform/members-and-access)
22
22
 
23
23
  ## Deep links work — routing is yours
24
24
 
25
- When a path doesn't match a file, Canopy serves your `index.html` instead of a 404. That's what makes client-side routing work: someone can bookmark
26
- `/reports/42`, reload it, and your app boots and handles the route.
25
+ A path is looked up in this order:
26
+
27
+ 1. **The exact file.** `/logo.svg` serves `logo.svg`.
28
+ 2. **A folder's `index.html`.** `/widgets/foo/` serves `widgets/foo/index.html`,
29
+ and `/widgets/foo` redirects to `/widgets/foo/` first. This is how
30
+ prerendered and multi-page builds work: every page is its own file, and
31
+ reloading any of them gets that page, not your homepage.
32
+ 3. **Your root `index.html`.** Anything else gets the app's entry point instead
33
+ of a 404. That's what makes client-side routing work: someone can bookmark
34
+ `/reports/42`, reload it, and your app boots and handles the route.
27
35
 
28
36
  The trade-off is that a **typo'd asset also gets `index.html`** rather than an
29
37
  error. If a script or image comes back as your homepage HTML, you're looking at
@@ -57,12 +65,13 @@ without deploying anything.
57
65
 
58
66
  ## What a deploy may contain
59
67
 
60
- - Up to **500 files** and **25 MB** in total.
68
+ - Up to **1000 files** and **50 MB** in total.
61
69
  - An `index.html` — a bundle without one is rejected.
62
70
  - Each file's type is fixed at deploy time from its extension. Unknown
63
71
  extensions are sent as a download rather than run as code.
64
72
 
65
- If you're over the file count, it's almost always un-bundled assets that a build
73
+ `canopy deploy` checks both limits before it uploads anything. If you're over
74
+ the file count, it's almost always un-bundled assets that a build
66
75
  step would combine; over the size limit, it's usually images or fonts that
67
76
  belong in [file storage](https://docs.canopy.website/sdk/files) instead of the bundle.
68
77
 
@@ -1,8 +1,8 @@
1
- import { t as version } from "./package-SWgTmctH.mjs";
2
- import { n as siteConfig } from "./site-DM2J10CB.mjs";
1
+ import { t as version } from "./package-XOtqboS1.mjs";
2
+ import { r as siteConfig } from "./site-0i7wAJqg.mjs";
3
3
  import { i as latestKnownVersion, r as compareSemver } from "./version-check-CcsA1QjI.mjs";
4
- import { n as agentHeaders } from "./api-BHzCvDcY.mjs";
5
- import { r as reportErrors } from "./ui-SBUdC9rX.mjs";
4
+ import { n as agentHeaders } from "./api-Bl6SG8Oq.mjs";
5
+ import { r as reportErrors } from "./ui-CLnICOne.mjs";
6
6
  import { defineCommand } from "citty";
7
7
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
8
8
  import { homedir } from "node:os";
@@ -1,6 +1,6 @@
1
- import { a as projectDirArgs, c as resolveProject } from "./site-DM2J10CB.mjs";
2
- import { a as apiRequest, l as withContext } from "./api-BHzCvDcY.mjs";
3
- import { a as unwrap, i as requireTty, r as reportErrors, t as formatDateTime } from "./ui-SBUdC9rX.mjs";
1
+ import { l as resolveProject, o as projectDirArgs } from "./site-0i7wAJqg.mjs";
2
+ import { a as apiRequest, f as withContext } from "./api-Bl6SG8Oq.mjs";
3
+ import { a as unwrap, i as requireTty, r as reportErrors, t as formatDateTime } from "./ui-CLnICOne.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  import * as p from "@clack/prompts";
6
6
  //#region src/egress.ts
@@ -1,7 +1,7 @@
1
- import { t as version } from "./package-SWgTmctH.mjs";
2
- import { a as projectDirArgs, c as resolveProject, d as tryResolveProject, n as siteConfig, u as tryReadCanopyJson } from "./site-DM2J10CB.mjs";
3
- import { a as apiRequest, c as credentials, i as apiDownload, o as apiUpload, s as commonHeaders } from "./api-BHzCvDcY.mjs";
4
- import { r as reportErrors, t as formatDateTime } from "./ui-SBUdC9rX.mjs";
1
+ import { t as version } from "./package-XOtqboS1.mjs";
2
+ import { f as tryReadCanopyJson, l as resolveProject, o as projectDirArgs, p as tryResolveProject, r as siteConfig } from "./site-0i7wAJqg.mjs";
3
+ import { a as apiRequest, c as credentials, i as apiDownload, o as apiUpload, s as commonHeaders } from "./api-Bl6SG8Oq.mjs";
4
+ import { r as reportErrors, t as formatDateTime } from "./ui-CLnICOne.mjs";
5
5
  import { defineCommand } from "citty";
6
6
  import { basename, extname, join } from "node:path";
7
7
  import { readFile, writeFile } from "node:fs/promises";