@mediamonks/canopy-cli 0.7.35 → 0.7.37

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 (35) hide show
  1. package/dist/{ai-DHLnyqfq.mjs → ai-CjvrcBIz.mjs} +2 -2
  2. package/dist/{api-C4rMhE7i.mjs → api-BPEYm9ts.mjs} +21 -0
  3. package/dist/{browse-Dycwj0V_.mjs → browse-CBelCgnW.mjs} +1 -1
  4. package/dist/{db-BB6O6085.mjs → db-CbqgjfYz.mjs} +159 -13
  5. package/dist/{deploy-Ka2lHkyy.mjs → deploy-CqCyry4o.mjs} +3 -3
  6. package/dist/{dev-oS4nYMSn.mjs → dev-BtnEuAnS.mjs} +36 -14
  7. package/dist/dev-D_qzeLng.mjs +2 -0
  8. package/dist/docs/cli-db.md +30 -1
  9. package/dist/docs/data.md +11 -0
  10. package/dist/docs/platform-project-data.md +23 -3
  11. package/dist/{docs-CGrXLl7G.mjs → docs-DvCUaI1O.mjs} +2 -2
  12. package/dist/{egress-If_Gsx_M.mjs → egress-ButFsQ2N.mjs} +2 -2
  13. package/dist/{feedback-CrmDfqmZ.mjs → feedback-CIjjiEaj.mjs} +3 -3
  14. package/dist/index.mjs +30 -30
  15. package/dist/{local-Cc8H3NUw.mjs → local-BfqTheMP.mjs} +2 -2
  16. package/dist/{login-D0J5s_Ex.mjs → login-CtDWXICZ.mjs} +3 -3
  17. package/dist/{logs-DQQOawOG.mjs → logs-BltUi4Gp.mjs} +2 -2
  18. package/dist/{mcp-DAaO4SQc.mjs → mcp-DTUJV4FT.mjs} +2 -2
  19. package/dist/{migrations-Czlcddyz.mjs → migrations-CF1iNZ7F.mjs} +1 -1
  20. package/dist/{open-DcaQKi1v.mjs → open-DXo60Ikf.mjs} +1 -1
  21. package/dist/{package-BOXNJrWP.mjs → package-BY69Tjq_.mjs} +1 -1
  22. package/dist/{projects-DQAhCZm8.mjs → projects-DPGRI-SI.mjs} +3 -3
  23. package/dist/{repo-Bb9ntNa4.mjs → repo-CKIQrrHK.mjs} +2 -2
  24. package/dist/repo-DvrIsubH.mjs +2 -0
  25. package/dist/{schedule-C5lMf3ny.mjs → schedule-DdMq5eXC.mjs} +2 -2
  26. package/dist/{secrets-BSEjZnxg.mjs → secrets-BQYaPSOT.mjs} +2 -2
  27. package/dist/{skill-GPJrYL61.mjs → skill-B96cKy8K.mjs} +2 -2
  28. package/dist/{staging-BZVty57b.mjs → staging-CU4QrzFX.mjs} +2 -2
  29. package/dist/{token-HHlUR0CF.mjs → token-BWnLsWLg.mjs} +2 -2
  30. package/dist/{ui-C_jSc1Kz.mjs → ui-CHfKV6lG.mjs} +1 -1
  31. package/dist/{upgrade-DSTg2cgu.mjs → upgrade-SfxmPh3F.mjs} +2 -2
  32. package/dist/{usage-CUYrWxv0.mjs → usage-Cqqvnzrf.mjs} +2 -2
  33. package/package.json +1 -1
  34. package/dist/dev-CID2LCES.mjs +0 -2
  35. package/dist/repo-1RQ1vBMv.mjs +0 -2
@@ -1,5 +1,5 @@
1
- import { i as apiRequest, o as withContext } from "./api-C4rMhE7i.mjs";
2
- import { r as reportErrors } from "./ui-C_jSc1Kz.mjs";
1
+ import { i as apiRequest, o as withContext } from "./api-BPEYm9ts.mjs";
2
+ import { r as reportErrors } from "./ui-CHfKV6lG.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  //#region src/ai.ts
5
5
  /**
@@ -92,6 +92,26 @@ const ERROR_COPY = {
92
92
  why: "the description is invalid (at most 1000 characters)",
93
93
  fix: "Shorten the description to 1000 characters or fewer."
94
94
  },
95
+ user_session_required: {
96
+ why: "this needs your own sign-in, and a project-scoped token (dev, agent, worker, deploy) was used",
97
+ fix: "Run it yourself after `canopy login`, or use the dashboard."
98
+ },
99
+ invalid_retention: {
100
+ why: "retention must be a whole number of days from 1 to 365, or off",
101
+ fix: "Pass e.g. `--retention 30`, or `--retention off` to keep snapshots until deleted."
102
+ },
103
+ retention_shorter_than_schedule: {
104
+ why: "retention is shorter than the schedule, so some days would have no snapshot at all",
105
+ fix: "Use at least 1 day for a daily schedule and 7 for a weekly one."
106
+ },
107
+ invalid_schedule: {
108
+ why: "the schedule must be off, daily or weekly",
109
+ fix: "Pass `--schedule off`, `--schedule daily` or `--schedule weekly`."
110
+ },
111
+ invalid_retention_scope: {
112
+ why: "the retention scope must be all or scheduled",
113
+ fix: "Pass `--retention-scope all` (every snapshot) or `--retention-scope scheduled`."
114
+ },
95
115
  snapshot_not_found: {
96
116
  why: "no snapshot with that id exists for this project",
97
117
  fix: "Run `canopy db snapshots` to list available snapshots."
@@ -335,6 +355,7 @@ async function apiDataRequest(method, path, projectSlug, body, rawContentType, c
335
355
  body: body === void 0 ? void 0 : rawContentType !== void 0 ? body : JSON.stringify(body)
336
356
  });
337
357
  if (!response.ok) return fail(response, isDeployToken, method, path);
358
+ if (response.status === 204) return void 0;
338
359
  return await response.json();
339
360
  }
340
361
  //#endregion
@@ -1,5 +1,5 @@
1
1
  import { d as tryResolveProject, n as siteConfig } from "./site-DM2J10CB.mjs";
2
- import { i as apiRequest, t as ApiError } from "./api-C4rMhE7i.mjs";
2
+ import { i as apiRequest, t as ApiError } from "./api-BPEYm9ts.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  //#region src/browse.ts
5
5
  /**
@@ -1,7 +1,7 @@
1
1
  import { c as resolveProject, o as projectFlagArgs } from "./site-DM2J10CB.mjs";
2
- import { i as apiRequest, n as apiDataRequest, o as withContext } from "./api-C4rMhE7i.mjs";
3
- import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-C_jSc1Kz.mjs";
4
- import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-Czlcddyz.mjs";
2
+ import { i as apiRequest, n as apiDataRequest, o as withContext } from "./api-BPEYm9ts.mjs";
3
+ import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-CHfKV6lG.mjs";
4
+ import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-CF1iNZ7F.mjs";
5
5
  import { defineCommand } from "citty";
6
6
  import { readFileSync, writeFileSync } from "node:fs";
7
7
  import * as p from "@clack/prompts";
@@ -12,7 +12,8 @@ import * as p from "@clack/prompts";
12
12
  *
13
13
  * pull download a fresh SQL dump of the project database
14
14
  * snapshot store a dump server-side (restorable from CLI or dashboard)
15
- * snapshots list stored snapshots
15
+ * snapshots list stored snapshots; `snapshots delete` and `snapshots settings`
16
+ * (schedule and retention, OpenSpec snapshot-schedules)
16
17
  * restore replace ALL project data from a snapshot or a local .sql file
17
18
  * migrate apply pending schema migrations (docs/13-migrations-and-drizzle.md)
18
19
  * access show or set what the app's pages may do with the database (docs/35)
@@ -84,25 +85,170 @@ const snapshotCommand = defineCommand({
84
85
  console.log(`Restore anytime with \`canopy db restore --snapshot ${created.key}\`.`);
85
86
  })
86
87
  });
87
- const snapshotsCommand = defineCommand({
88
+ /** `--retention`: a whole number of days, or off. Range is the server's call. */
89
+ function parseRetention(value) {
90
+ const v = value.trim().toLowerCase();
91
+ if (v === "off" || v === "none") return null;
92
+ if (!/^\d+$/.test(v)) throw new Error(`--retention takes a number of days or "off", not "${value}".`);
93
+ return Number(v);
94
+ }
95
+ const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
96
+ /** One line on when scheduled snapshots happen, e.g. "daily at 03:00 UTC". */
97
+ function describeSchedule(settings) {
98
+ if (settings.schedule === "off") return "off";
99
+ const time = `${String(settings.slot.hour).padStart(2, "0")}:00 UTC`;
100
+ return settings.schedule === "weekly" && settings.slot.weekday ? `weekly on ${capitalize(settings.slot.weekday)} at ${time}` : `daily at ${time}`;
101
+ }
102
+ function describeRetention(settings) {
103
+ if (settings.retentionDays === null) return "off (snapshots are kept until deleted)";
104
+ const days = `${settings.retentionDays} day${settings.retentionDays === 1 ? "" : "s"}`;
105
+ return settings.retentionScope === "scheduled" ? `${days}, scheduled snapshots only` : `${days}, all snapshots (manual, scheduled, pre-migrate, pre-promote)`;
106
+ }
107
+ function describeLastRun(lastRun) {
108
+ if (!lastRun) return "never";
109
+ return lastRun.ok ? `${lastRun.at} — stored ${lastRun.detail ?? ""}`.trimEnd() : `${lastRun.at} — FAILED: ${lastRun.detail ?? "unknown error"}`;
110
+ }
111
+ function printSettings(slug, settings) {
112
+ console.log(`${slug} snapshot settings`);
113
+ console.log(` schedule ${describeSchedule(settings)}`);
114
+ console.log(` retention ${describeRetention(settings)}`);
115
+ if (settings.schedule !== "off" || settings.lastRun) console.log(` last run ${describeLastRun(settings.lastRun)}`);
116
+ }
117
+ let snapshotsSubcommandRan = false;
118
+ const snapshotsDeleteCommand = defineCommand({
88
119
  meta: {
89
- name: "snapshots",
90
- description: "List stored snapshots"
120
+ name: "delete",
121
+ description: "Delete a stored snapshot for good"
91
122
  },
92
123
  args: {
93
124
  ...projectFlagArgs,
94
- ...stagingFlagArgs
125
+ ...stagingFlagArgs,
126
+ key: {
127
+ type: "positional",
128
+ description: "Snapshot key (from `canopy db snapshots`)",
129
+ required: true
130
+ },
131
+ yes: {
132
+ type: "boolean",
133
+ default: false,
134
+ description: "Skip the confirmation"
135
+ }
136
+ },
137
+ run: reportErrors("db snapshots delete", async ({ args }) => {
138
+ snapshotsSubcommandRan = true;
139
+ const { slug } = resolveProject(args);
140
+ if (!args.yes) {
141
+ requireTty("pass --yes to delete without confirmation.");
142
+ if (!unwrap(await p.confirm({
143
+ message: `Delete snapshot ${args.key} of "${slug}"${channelNote(args)} for good? It can't be restored afterwards.`,
144
+ initialValue: false
145
+ }))) {
146
+ p.cancel("Aborted.");
147
+ return;
148
+ }
149
+ }
150
+ await withContext(apiDataRequest("DELETE", `/db/snapshots/${encodeURIComponent(args.key)}`, slug, void 0, void 0, channelOf(args)), { action: `delete snapshot ${args.key} of "${slug}"` });
151
+ p.log.success(`Deleted ${args.key}${channelNote(args)}.`);
152
+ })
153
+ });
154
+ const snapshotsSettingsCommand = defineCommand({
155
+ meta: {
156
+ name: "settings",
157
+ description: "Show or change the snapshot schedule and retention"
158
+ },
159
+ args: {
160
+ ...projectFlagArgs,
161
+ schedule: {
162
+ type: "string",
163
+ description: "off | daily | weekly"
164
+ },
165
+ retention: {
166
+ type: "string",
167
+ description: "Delete snapshots older than this many days (1–365), or off"
168
+ },
169
+ "retention-scope": {
170
+ type: "string",
171
+ description: "Which snapshots retention deletes: all (default) | scheduled"
172
+ },
173
+ yes: {
174
+ type: "boolean",
175
+ default: false,
176
+ description: "Don't ask before a retention change that deletes snapshots"
177
+ },
178
+ json: {
179
+ type: "boolean",
180
+ default: false,
181
+ description: "Print the settings as JSON"
182
+ }
95
183
  },
96
- run: reportErrors("db snapshots", async ({ args }) => {
184
+ run: reportErrors("db snapshots settings", async ({ args }) => {
185
+ snapshotsSubcommandRan = true;
97
186
  const { slug } = resolveProject(args);
98
- const { snapshots } = await apiDataRequest("GET", "/db/snapshots", slug, void 0, void 0, channelOf(args));
99
- if (snapshots.length === 0) {
100
- console.log(`No snapshots for ${slug} yet — run \`canopy db snapshot\`.`);
187
+ const path = `/projects/${slug}/snapshot-settings`;
188
+ const change = {};
189
+ if (args.schedule !== void 0) change.schedule = args.schedule.trim().toLowerCase();
190
+ if (args.retention !== void 0) change.retentionDays = parseRetention(args.retention);
191
+ if (args["retention-scope"] !== void 0) change.retentionScope = args["retention-scope"].trim().toLowerCase();
192
+ if (Object.keys(change).length === 0) {
193
+ const settings = await withContext(apiRequest("GET", path), { action: `read the snapshot settings of "${slug}"` });
194
+ if (args.json) console.log(JSON.stringify(settings, null, 2));
195
+ else printSettings(slug, settings);
196
+ return;
197
+ }
198
+ const context = { action: `change the snapshot settings of "${slug}"` };
199
+ const preview = await withContext(apiRequest("PUT", path, {
200
+ ...change,
201
+ dryRun: true
202
+ }), context);
203
+ if ((preview.wouldExpire ?? 0) > 0 && !args.yes) {
204
+ requireTty("pass --yes to apply a retention that deletes existing snapshots.");
205
+ const n = preview.wouldExpire;
206
+ if (!unwrap(await p.confirm({
207
+ message: `This deletes ${n} existing snapshot${n === 1 ? "" : "s"} of "${slug}" within the hour. Continue?`,
208
+ initialValue: false
209
+ }))) {
210
+ p.cancel("Aborted.");
211
+ return;
212
+ }
213
+ }
214
+ const settings = await withContext(apiRequest("PUT", path, change), context);
215
+ if (args.json) {
216
+ console.log(JSON.stringify(settings, null, 2));
101
217
  return;
102
218
  }
103
- for (const snapshot of snapshots) console.log(`${snapshot.key} ${formatSize(snapshot.size).padStart(9)} ${snapshot.createdAt}`);
219
+ printSettings(slug, settings);
220
+ if ((settings.wouldExpire ?? 0) > 0) console.log(`\n${settings.wouldExpire} snapshot(s) past retention will be deleted within the hour.`);
104
221
  })
105
222
  });
223
+ const listSnapshotsRun = reportErrors("db snapshots", async ({ args }) => {
224
+ const { slug } = resolveProject(args);
225
+ const [{ snapshots }, settings] = await Promise.all([apiDataRequest("GET", "/db/snapshots", slug, void 0, void 0, channelOf(args)), apiRequest("GET", `/projects/${slug}/snapshot-settings`).catch(() => null)]);
226
+ if (settings) {
227
+ console.log(`Schedule: ${describeSchedule(settings)} · retention: ${describeRetention(settings)}`);
228
+ if (settings.lastRun) console.log(`Last scheduled run: ${describeLastRun(settings.lastRun)}`);
229
+ console.log("");
230
+ }
231
+ if (snapshots.length === 0) {
232
+ console.log(`No snapshots for ${slug}${channelNote(args)} yet — run \`canopy db snapshot\`.`);
233
+ return;
234
+ }
235
+ for (const snapshot of snapshots) console.log(`${snapshot.key} ${(snapshot.kind ?? "").padEnd(11)} ${formatSize(snapshot.size).padStart(9)} ${snapshot.createdAt}`);
236
+ });
237
+ const snapshotsCommand = defineCommand({
238
+ meta: {
239
+ name: "snapshots",
240
+ description: "List stored snapshots (and delete, settings)"
241
+ },
242
+ args: {
243
+ ...projectFlagArgs,
244
+ ...stagingFlagArgs
245
+ },
246
+ subCommands: {
247
+ delete: snapshotsDeleteCommand,
248
+ settings: snapshotsSettingsCommand
249
+ },
250
+ run: (context) => snapshotsSubcommandRan ? void 0 : listSnapshotsRun(context)
251
+ });
106
252
  const restoreCommand = defineCommand({
107
253
  meta: {
108
254
  name: "restore",
@@ -1,7 +1,7 @@
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 { i as apiRequest, n as apiDataRequest, o as withContext, r as apiDownload } from "./api-C4rMhE7i.mjs";
3
- import { a as unwrap, r as reportErrors, t as formatDateTime } from "./ui-C_jSc1Kz.mjs";
4
- import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-Czlcddyz.mjs";
2
+ import { i as apiRequest, n as apiDataRequest, o as withContext, r as apiDownload } from "./api-BPEYm9ts.mjs";
3
+ import { a as unwrap, r as reportErrors, t as formatDateTime } from "./ui-CHfKV6lG.mjs";
4
+ import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-CF1iNZ7F.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";
@@ -1,7 +1,7 @@
1
1
  import { a as projectDirArgs, c as resolveProject, i as projectDialect, n as siteConfig, u as tryReadCanopyJson } from "./site-DM2J10CB.mjs";
2
- import { i as apiRequest, o as withContext, t as ApiError } from "./api-C4rMhE7i.mjs";
3
- import { r as reportErrors, t as formatDateTime } from "./ui-C_jSc1Kz.mjs";
4
- import { r as readMigrationFiles } from "./migrations-Czlcddyz.mjs";
2
+ import { i as apiRequest, o as withContext, t as ApiError } from "./api-BPEYm9ts.mjs";
3
+ import { r as reportErrors, t as formatDateTime } from "./ui-CHfKV6lG.mjs";
4
+ import { r as readMigrationFiles } from "./migrations-CF1iNZ7F.mjs";
5
5
  import { i as stampWorkerHeaders, n as WORKER_ATTEST_HEADER } from "./src-CxzGOEhP.mjs";
6
6
  import { n as writeDevServerRecord } from "./dev-server-file-Dk9pk4AR.mjs";
7
7
  import { createRequire } from "node:module";
@@ -34,7 +34,7 @@ function workerDialect(flags) {
34
34
  * gateway serves it in production (dev-worker.ts). Returns the plugin plus
35
35
  * the notice lines to print, or undefined when the project has no worker.
36
36
  */
37
- async function workerPlugin(flags, slug, workerAttest) {
37
+ async function workerPlugin(flags, slug, workerAttest, remoteUser) {
38
38
  const entry = tryReadCanopyJson(flags.dir)?.worker;
39
39
  if (entry === void 0) return void 0;
40
40
  if (!existsSync(join(flags.dir, entry))) {
@@ -44,17 +44,18 @@ async function workerPlugin(flags, slug, workerAttest) {
44
44
  const { missingSecretNames, readDevVars, workerDevPlugin } = await import("./dev-worker-CpSB_Okh.mjs");
45
45
  const secrets = readDevVars(flags.dir);
46
46
  const email = siteConfig().email;
47
+ const user = await remoteUser?.() ?? (email ? {
48
+ sub: "local-dev",
49
+ email,
50
+ name: "Local Dev"
51
+ } : null);
47
52
  const plugin = workerDevPlugin({
48
53
  dir: flags.dir,
49
54
  entry,
50
55
  slug,
51
56
  channel: flags.staging ? "staging" : "live",
52
57
  secrets,
53
- user: email ? {
54
- sub: "local-dev",
55
- email,
56
- name: "Local Dev"
57
- } : null,
58
+ user,
58
59
  dialect: workerDialect(flags),
59
60
  workerAttest
60
61
  });
@@ -156,6 +157,26 @@ function devProxyHeaders(token, slug, channel) {
156
157
  return Object.fromEntries(headers);
157
158
  }
158
159
  /**
160
+ * The signed-in person as the platform sees them — what auth.user() answers
161
+ * in the browser, so env.USER in a live-data dev worker matches it.
162
+ * Undefined on any failure: dev still starts, with the local stub.
163
+ */
164
+ async function fetchDevUser(apiUrl, headers) {
165
+ try {
166
+ const res = await fetch(`${apiUrl}/data/v1/auth/me`, { headers });
167
+ if (!res.ok) return void 0;
168
+ const me = await res.json();
169
+ if (!me.id) return void 0;
170
+ return {
171
+ sub: me.id,
172
+ email: me.email ?? "",
173
+ name: me.name ?? ""
174
+ };
175
+ } catch {
176
+ return;
177
+ }
178
+ }
179
+ /**
159
180
  * One request from Vite's /_api proxy into the --local sandbox. The response
160
181
  * body is streamed, not buffered — realtime subscriptions (docs/48) are
161
182
  * long-lived SSE responses — and a client that goes away aborts the sandbox
@@ -202,11 +223,12 @@ async function bridgeToSandbox(sandbox, req, res) {
202
223
  async function devRemote(flags, slug) {
203
224
  const { token, expires_at } = await withContext(apiRequest("POST", `/projects/${slug}/dev-token`), devTokenContext(slug));
204
225
  const { apiUrl } = siteConfig();
205
- const worker = await workerPlugin(flags, slug);
226
+ const channel = flags.staging ? "staging" : "live";
227
+ const worker = await workerPlugin(flags, slug, void 0, () => fetchDevUser(apiUrl, devProxyHeaders(token, slug, channel)));
206
228
  const server = await startVite(flags, { proxy: { "/_api": {
207
229
  target: apiUrl,
208
230
  changeOrigin: true,
209
- headers: devProxyHeaders(token, slug, flags.staging ? "staging" : "live"),
231
+ headers: devProxyHeaders(token, slug, channel),
210
232
  rewrite: rewriteApiPath
211
233
  } } }, worker ? [worker.plugin] : []);
212
234
  if (flags.staging) {
@@ -229,7 +251,7 @@ async function devRemote(flags, slug) {
229
251
  async function devLocal(flags, slug) {
230
252
  let dump;
231
253
  if (flags.pull) {
232
- const { fetchProjectDump } = await import("./local-Cc8H3NUw.mjs");
254
+ const { fetchProjectDump } = await import("./local-BfqTheMP.mjs");
233
255
  console.log(`Pulling a fresh export of ${slug}${flags.staging ? " [staging]" : ""}…`);
234
256
  dump = await fetchProjectDump(slug, flags.staging ? "staging" : "live");
235
257
  }
@@ -245,7 +267,7 @@ async function devLocal(flags, slug) {
245
267
  console.log(`Note: ai/services will be unavailable — could not reach the platform (${reason}).`);
246
268
  }
247
269
  const workerAttest = randomBytes(24).toString("base64url");
248
- const { createSandbox } = await import("./local-Cc8H3NUw.mjs");
270
+ const { createSandbox } = await import("./local-BfqTheMP.mjs");
249
271
  const sandbox = await createSandbox({
250
272
  slug,
251
273
  persistDir: join(flags.dir, ".canopy", "local"),
@@ -354,4 +376,4 @@ const devCommand = defineCommand({
354
376
  })
355
377
  });
356
378
  //#endregion
357
- export { rewriteApiPath as i, devCommand as n, devProxyHeaders as r, bridgeToSandbox as t };
379
+ export { rewriteApiPath as a, fetchDevUser as i, devCommand as n, devProxyHeaders as r, bridgeToSandbox as t };
@@ -0,0 +1,2 @@
1
+ import { n as devCommand } from "./dev-BtnEuAnS.mjs";
2
+ export { devCommand };
@@ -13,6 +13,9 @@ The habit that makes restores safe: **always `canopy db snapshot` before `canopy
13
13
  canopy db pull [--out <file>] [--project <slug>]
14
14
  canopy db snapshot [--project <slug>]
15
15
  canopy db snapshots [--project <slug>]
16
+ canopy db snapshots delete <key> [--yes] [--project <slug>]
17
+ canopy db snapshots settings [--schedule off|daily|weekly] [--retention <days>|off]
18
+ [--retention-scope all|scheduled] [--yes] [--json] [--project <slug>]
16
19
  canopy db restore [file.sql] [--snapshot <key>] [--yes] [--project <slug>]
17
20
  canopy db migrate [--plan] [--project <slug>]
18
21
  ```
@@ -37,7 +40,27 @@ Stores a dump **server-side**, restorable anytime from the CLI or the dashboard.
37
40
 
38
41
  ### canopy db snapshots
39
42
 
40
- Lists stored snapshots for the project — one line per snapshot with its key, size, and creation time. If there are none yet, it points you at `canopy db snapshot`.
43
+ Lists stored snapshots for the project — one line per snapshot with its key, kind, size, and creation time — under a line with the project's schedule, retention and last scheduled run. The kind says what made it: `manual` (you), `scheduled` (Canopy, on the schedule), `pre-migrate` or `pre-promote` (Canopy, before applying migrations). If there are none yet, it points you at `canopy db snapshot`.
44
+
45
+ ### canopy db snapshots delete
46
+
47
+ Deletes one stored snapshot for good: it can't be restored afterwards. Asks for confirmation unless `--yes` is passed. Use `--staging` for a snapshot in the staging history.
48
+
49
+ Deleting needs your own sign-in (`canopy login`). Project tokens, such as the ones `canopy dev` or an agent uses, can't delete snapshots.
50
+
51
+ ### canopy db snapshots settings
52
+
53
+ Shows or changes when Canopy takes snapshots by itself and how long snapshots are kept. With no flags it prints the current settings.
54
+
55
+ - `--schedule off|daily|weekly` — Canopy snapshots the live database once a day or once a week, at a fixed time it picks for the project (shown in UTC). The staging database is not snapshotted on a schedule.
56
+ - `--retention <days>` — delete snapshots older than this many days (1 to 365), once a day. `--retention off` keeps snapshots until you delete them. Retention works with or without a schedule, and covers both the live and the staging history.
57
+ - `--retention-scope all|scheduled` — which snapshots retention deletes: `all` (the default) includes the ones you took by hand and the ones taken before migrations; `scheduled` only deletes the ones Canopy took on the schedule.
58
+ - `--yes` — when a new retention would delete existing snapshots, the CLI says how many and asks first. `--yes` skips that.
59
+ - `--json` — print the settings as JSON.
60
+
61
+ A daily schedule needs at least 1 day of retention and a weekly one at least 7, so there is never a stretch without a snapshot. Like deleting, changing the settings needs your own sign-in.
62
+
63
+ Canopy also keeps short-lived backups of its own databases and storage for disaster recovery. Those expire on their own within 30 days and can't be restored from the CLI.
41
64
 
42
65
  ### canopy db restore
43
66
 
@@ -73,6 +96,12 @@ canopy db snapshot && canopy db pull
73
96
 
74
97
  Belt and braces before risky work: a server-side snapshot you can restore from the dashboard, plus a local dump on disk.
75
98
 
99
+ ```bash
100
+ canopy db snapshots settings --schedule daily --retention 30
101
+ ```
102
+
103
+ A daily backup, with snapshots older than 30 days deleted automatically.
104
+
76
105
  ```bash
77
106
  canopy db restore --snapshot snap-2026-08-11-1200 --yes --project my-app
78
107
  ```
package/dist/docs/data.md CHANGED
@@ -61,6 +61,17 @@ canopy db snapshots # list stored snapshots
61
61
  Snapshots live on the platform and can be restored at any time — handy before a
62
62
  risky change.
63
63
 
64
+ Canopy can also take them for you, and clean up old ones:
65
+
66
+ ```bash
67
+ canopy db snapshots settings --schedule daily --retention 30 # daily backup, keep 30 days
68
+ canopy db snapshots settings --retention-scope scheduled # never auto-delete your own
69
+ canopy db snapshots delete <key> # delete one for good
70
+ ```
71
+
72
+ The same settings are on the dashboard's Data tab. See
73
+ [`canopy db`](https://docs.canopy.website/cli/db#canopy-db-snapshots-settings) for every option.
74
+
64
75
  ## Restore — destructive
65
76
 
66
77
  > **Warning:** Restoring **replaces ALL project data**. Take a snapshot first — a snapshot taken seconds earlier
@@ -45,8 +45,28 @@ The Data tab's **Data safety** card is your undo button for data:
45
45
  confirm.
46
46
  - **Export** downloads a dump to your machine.
47
47
 
48
- Snapshots live server-side and stay available until you delete the project,
49
- so you can restore from days ago, not just from your last session.
48
+ Snapshots live server-side, so you can restore from days ago, not just from
49
+ your last session. Each one shows what made it: you, the schedule, or Canopy
50
+ before applying migrations. You can delete one you no longer need; it's gone
51
+ for good.
52
+
53
+ ## Scheduled snapshots and retention
54
+
55
+ In the same card you can have Canopy take a snapshot of the live database
56
+ **daily** or **weekly**, at a fixed time it picks for the project. You don't
57
+ need a worker or any code for it.
58
+
59
+ **Retention** deletes snapshots older than a number of days you choose, once a
60
+ day. Choose whether it applies to all snapshots or only the scheduled ones, so
61
+ your own restore points stay until you delete them. Retention also works
62
+ without a schedule. Without retention, snapshots stay until you delete them or
63
+ the project.
64
+
65
+ Changing these settings and deleting snapshots needs a project member signed
66
+ in as themselves.
67
+
68
+ Canopy also keeps short-lived backups of its own databases and storage for
69
+ disaster recovery. Those expire on their own within 30 days.
50
70
 
51
71
  > **Warning:** Restoring replaces **all** current data with the snapshot's contents. Take a fresh snapshot first
52
72
  > if there's anything recent you might want back.
@@ -58,7 +78,7 @@ Everything above is also available to you (and your coding agent) via the CLI:
58
78
  ```bash
59
79
  canopy db pull # download a fresh SQL dump
60
80
  canopy db snapshot # store a dump server-side
61
- canopy db snapshots # list stored snapshots
81
+ canopy db snapshots # list stored snapshots (delete, settings)
62
82
  canopy db restore # replace project data with a snapshot
63
83
  canopy db access # what the app's own pages may do with the database
64
84
  ```
@@ -1,7 +1,7 @@
1
- import { t as version } from "./package-BOXNJrWP.mjs";
1
+ import { t as version } from "./package-BY69Tjq_.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-C_jSc1Kz.mjs";
4
+ import { r as reportErrors } from "./ui-CHfKV6lG.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,6 +1,6 @@
1
1
  import { a as projectDirArgs, c as resolveProject } from "./site-DM2J10CB.mjs";
2
- import { i as apiRequest, o as withContext } from "./api-C4rMhE7i.mjs";
3
- import { a as unwrap, i as requireTty, r as reportErrors, t as formatDateTime } from "./ui-C_jSc1Kz.mjs";
2
+ import { i as apiRequest, o as withContext } from "./api-BPEYm9ts.mjs";
3
+ import { a as unwrap, i as requireTty, r as reportErrors, t as formatDateTime } from "./ui-CHfKV6lG.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-BOXNJrWP.mjs";
1
+ import { t as version } from "./package-BY69Tjq_.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 { a as apiUpload, i as apiRequest, r as apiDownload } from "./api-C4rMhE7i.mjs";
4
- import { r as reportErrors, t as formatDateTime } from "./ui-C_jSc1Kz.mjs";
3
+ import { a as apiUpload, i as apiRequest, r as apiDownload } from "./api-BPEYm9ts.mjs";
4
+ import { r as reportErrors, t as formatDateTime } from "./ui-CHfKV6lG.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";
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as version } from "./package-BOXNJrWP.mjs";
2
+ import { t as version } from "./package-BY69Tjq_.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";
@@ -63,35 +63,35 @@ const main = defineCommand({
63
63
  description: "canopy — create and deploy vibecoded projects"
64
64
  },
65
65
  subCommands: {
66
- login: () => import("./login-D0J5s_Ex.mjs").then((m) => m.loginCommand),
67
- whoami: () => import("./login-D0J5s_Ex.mjs").then((m) => m.whoamiCommand),
68
- logout: () => import("./login-D0J5s_Ex.mjs").then((m) => m.logoutCommand),
69
- create: () => import("./projects-DQAhCZm8.mjs").then((m) => m.createCommand),
70
- init: () => import("./projects-DQAhCZm8.mjs").then((m) => m.initCommand),
71
- projects: () => import("./projects-DQAhCZm8.mjs").then((m) => m.projectsCommand),
72
- delete: () => import("./projects-DQAhCZm8.mjs").then((m) => m.deleteCommand),
73
- dev: () => import("./dev-CID2LCES.mjs").then((m) => m.devCommand),
74
- deploy: () => import("./deploy-Ka2lHkyy.mjs").then((m) => m.deployCommand),
75
- promote: () => import("./deploy-Ka2lHkyy.mjs").then((m) => m.promoteCommand),
76
- staging: () => import("./staging-BZVty57b.mjs").then((m) => m.stagingCommand),
77
- rollback: () => import("./deploy-Ka2lHkyy.mjs").then((m) => m.rollbackCommand),
78
- download: () => import("./deploy-Ka2lHkyy.mjs").then((m) => m.downloadCommand),
79
- token: () => import("./token-HHlUR0CF.mjs").then((m) => m.tokenCommand),
80
- egress: () => import("./egress-If_Gsx_M.mjs").then((m) => m.egressCommand),
81
- secrets: () => import("./secrets-BSEjZnxg.mjs").then((m) => m.secretsCommand),
82
- schedule: () => import("./schedule-C5lMf3ny.mjs").then((m) => m.scheduleCommand),
83
- feedback: () => import("./feedback-CrmDfqmZ.mjs").then((m) => m.feedbackCommand),
84
- db: () => import("./db-BB6O6085.mjs").then((m) => m.dbCommand),
85
- usage: () => import("./usage-CUYrWxv0.mjs").then((m) => m.usageCommand),
86
- ai: () => import("./ai-DHLnyqfq.mjs").then((m) => m.aiCommand),
87
- open: () => import("./open-DcaQKi1v.mjs").then((m) => m.openCommand),
88
- browse: () => import("./browse-Dycwj0V_.mjs").then((m) => m.browseCommand),
89
- docs: () => import("./docs-CGrXLl7G.mjs").then((m) => m.docsCommand),
90
- skill: () => import("./skill-GPJrYL61.mjs").then((m) => m.skillCommand),
91
- upgrade: () => import("./upgrade-DSTg2cgu.mjs").then((m) => m.upgradeCommand),
92
- logs: () => import("./logs-DQQOawOG.mjs").then((m) => m.logsCommand),
93
- mcp: () => import("./mcp-DAaO4SQc.mjs").then((m) => m.mcpCommand),
94
- repo: () => import("./repo-1RQ1vBMv.mjs").then((m) => m.repoCommand)
66
+ login: () => import("./login-CtDWXICZ.mjs").then((m) => m.loginCommand),
67
+ whoami: () => import("./login-CtDWXICZ.mjs").then((m) => m.whoamiCommand),
68
+ logout: () => import("./login-CtDWXICZ.mjs").then((m) => m.logoutCommand),
69
+ create: () => import("./projects-DPGRI-SI.mjs").then((m) => m.createCommand),
70
+ init: () => import("./projects-DPGRI-SI.mjs").then((m) => m.initCommand),
71
+ projects: () => import("./projects-DPGRI-SI.mjs").then((m) => m.projectsCommand),
72
+ delete: () => import("./projects-DPGRI-SI.mjs").then((m) => m.deleteCommand),
73
+ dev: () => import("./dev-D_qzeLng.mjs").then((m) => m.devCommand),
74
+ deploy: () => import("./deploy-CqCyry4o.mjs").then((m) => m.deployCommand),
75
+ promote: () => import("./deploy-CqCyry4o.mjs").then((m) => m.promoteCommand),
76
+ staging: () => import("./staging-CU4QrzFX.mjs").then((m) => m.stagingCommand),
77
+ rollback: () => import("./deploy-CqCyry4o.mjs").then((m) => m.rollbackCommand),
78
+ download: () => import("./deploy-CqCyry4o.mjs").then((m) => m.downloadCommand),
79
+ token: () => import("./token-BWnLsWLg.mjs").then((m) => m.tokenCommand),
80
+ egress: () => import("./egress-ButFsQ2N.mjs").then((m) => m.egressCommand),
81
+ secrets: () => import("./secrets-BQYaPSOT.mjs").then((m) => m.secretsCommand),
82
+ schedule: () => import("./schedule-DdMq5eXC.mjs").then((m) => m.scheduleCommand),
83
+ feedback: () => import("./feedback-CIjjiEaj.mjs").then((m) => m.feedbackCommand),
84
+ db: () => import("./db-CbqgjfYz.mjs").then((m) => m.dbCommand),
85
+ usage: () => import("./usage-Cqqvnzrf.mjs").then((m) => m.usageCommand),
86
+ ai: () => import("./ai-CjvrcBIz.mjs").then((m) => m.aiCommand),
87
+ open: () => import("./open-DXo60Ikf.mjs").then((m) => m.openCommand),
88
+ browse: () => import("./browse-CBelCgnW.mjs").then((m) => m.browseCommand),
89
+ docs: () => import("./docs-DvCUaI1O.mjs").then((m) => m.docsCommand),
90
+ skill: () => import("./skill-B96cKy8K.mjs").then((m) => m.skillCommand),
91
+ upgrade: () => import("./upgrade-SfxmPh3F.mjs").then((m) => m.upgradeCommand),
92
+ logs: () => import("./logs-BltUi4Gp.mjs").then((m) => m.logsCommand),
93
+ mcp: () => import("./mcp-DTUJV4FT.mjs").then((m) => m.mcpCommand),
94
+ repo: () => import("./repo-DvrIsubH.mjs").then((m) => m.repoCommand)
95
95
  }
96
96
  });
97
97
  const quiet = process.argv[2] === "mcp" && process.argv[3] === "serve" || process.argv[2] === "repo" && process.argv[3] === "credential";
@@ -1,6 +1,6 @@
1
- import { n as apiDataRequest } from "./api-C4rMhE7i.mjs";
1
+ import { n as apiDataRequest } from "./api-BPEYm9ts.mjs";
2
2
  import { a as realtimeRouter, c as META_COLUMNS, l as blobRouter, n as WORKER_ATTEST_HEADER, o as dbRouter, s as hasPrivateNames } from "./src-CxzGOEhP.mjs";
3
- import { i as rewriteApiPath } from "./dev-oS4nYMSn.mjs";
3
+ import { a as rewriteApiPath } from "./dev-BtnEuAnS.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";