@mediamonks/canopy-cli 0.7.22 → 0.7.24

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 (44) hide show
  1. package/dist/db-jmYaum8F.mjs +251 -0
  2. package/dist/{deploy-DCggsLD7.mjs → deploy-CYKiBnrg.mjs} +3 -3
  3. package/dist/dev-C5BmSJo1.mjs +2 -0
  4. package/dist/{dev-CNqx_53I.mjs → dev-DVUEJ80c.mjs} +6 -6
  5. package/dist/{dev-worker-Bij9OUMk.mjs → dev-worker-CWU6XFe_.mjs} +35 -1
  6. package/dist/docs/ci.md +8 -1
  7. package/dist/docs/cli-egress.md +2 -1
  8. package/dist/docs/cli-login.md +2 -0
  9. package/dist/docs/cli-logout.md +9 -6
  10. package/dist/docs/cli-token.md +4 -2
  11. package/dist/docs/index.json +7 -2
  12. package/dist/docs/platform-ai-usage.md +36 -10
  13. package/dist/docs/platform-client-access.md +14 -2
  14. package/dist/docs/platform-egress.md +5 -3
  15. package/dist/docs/platform-members-and-access.md +29 -6
  16. package/dist/docs/platform-project-data.md +1 -0
  17. package/dist/docs/platform-security-headers.md +53 -0
  18. package/dist/docs/platform-slack.md +3 -1
  19. package/dist/docs/sdk-ai.md +102 -11
  20. package/dist/docs/sdk-database.md +30 -0
  21. package/dist/docs/sdk-files.md +6 -0
  22. package/dist/{docs-c0CdeBxh.mjs → docs-BLWio_65.mjs} +3 -3
  23. package/dist/{egress-BhbQfjjV.mjs → egress-BjN-7vil.mjs} +6 -3
  24. package/dist/{feedback-QhD8k6Ph.mjs → feedback-Bnmi1Ukv.mjs} +3 -3
  25. package/dist/index.mjs +26 -26
  26. package/dist/{local-CsSP4Vpt.mjs → local-CNPBZ89U.mjs} +38 -9
  27. package/dist/{login-BrEDKrb3.mjs → login-COZ7Xcm0.mjs} +57 -6
  28. package/dist/{logs-Beqz--Df.mjs → logs-B_YoflCq.mjs} +2 -2
  29. package/dist/{migrations-Bg9FEGFh.mjs → migrations-Btzr9_q7.mjs} +2 -2
  30. package/dist/{open-D7f1wNn3.mjs → open-D2hH933K.mjs} +2 -2
  31. package/dist/{package-BuwV_mje.mjs → package-DTm0EzxR.mjs} +1 -1
  32. package/dist/{projects-1r5387ui.mjs → projects-CLs6MEF7.mjs} +2 -2
  33. package/dist/{schedule-DK_Cv6Xf.mjs → schedule-B8zA8WlG.mjs} +2 -2
  34. package/dist/{site-BFaWfSVu.mjs → site-DM2J10CB.mjs} +1 -1
  35. package/dist/{skill-BvoYCIKk.mjs → skill-Dt49jdUT.mjs} +2 -2
  36. package/dist/{src-BKKAEkbH.mjs → src-BK0Hiwsn.mjs} +95 -3
  37. package/dist/{staging-CdpV2YHB.mjs → staging-pZpdZuj_.mjs} +2 -2
  38. package/dist/{token-C8Bmzwfl.mjs → token-BKwGa10J.mjs} +8 -4
  39. package/dist/{ui--atbZ-TM.mjs → ui-CUNkis89.mjs} +1 -1
  40. package/dist/{upgrade-0lmdjvom.mjs → upgrade-Cf81feok.mjs} +2 -2
  41. package/dist/{usage-C1-cvS2-.mjs → usage-UmzU6sGp.mjs} +2 -2
  42. package/package.json +3 -3
  43. package/dist/db-TaSLpQZ5.mjs +0 -204
  44. package/dist/dev-D7v7WK9O.mjs +0 -2
@@ -1,5 +1,5 @@
1
- import { c as resolveProject, o as projectFlagArgs } from "./site-BFaWfSVu.mjs";
2
- import { d as withContext, l as apiRequest, r as reportErrors, t as formatDateTime } from "./ui--atbZ-TM.mjs";
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";
3
3
  import { defineCommand } from "citty";
4
4
  //#region src/schedule.ts
5
5
  /**
@@ -170,7 +170,7 @@ function clearAllProfileTokens() {
170
170
  function projectUrl(slug) {
171
171
  const config = siteConfig();
172
172
  const apex = config.projectDomain?.trim();
173
- if (apex) return `${config.projectProtocol ?? (apex.includes("sslip.io") ? "http" : "https")}://${slug}.${apex.replace(/^\./, "")}`;
173
+ if (apex) return `${config.projectProtocol ?? "https"}://${slug}.${apex.replace(/^\./, "")}`;
174
174
  const apiHost = new URL(config.apiUrl).hostname;
175
175
  if (apiHost === "run.app" || apiHost.endsWith(".run.app") || apiHost.startsWith("api.")) throw new Error("Cannot derive the project URL. Set projectDomain (baked profile or CANOPY_PROJECT_DOMAIN).");
176
176
  return `https://${slug}.${apiHost.split(".").slice(-2).join(".")}`;
@@ -1,5 +1,5 @@
1
- import { t as version } from "./package-BuwV_mje.mjs";
2
- import { r as reportErrors } from "./ui--atbZ-TM.mjs";
1
+ import { t as version } from "./package-DTm0EzxR.mjs";
2
+ import { r as reportErrors } from "./ui-CUNkis89.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,4 +1,54 @@
1
1
  import { Hono } from "hono";
2
+ //#region ../data-plane/src/blob-headers.ts
3
+ /**
4
+ * Blob (SDK file store) serving policy. Blobs carry a caller-chosen content
5
+ * type and are served on the project's own origin, so a stored `text/html` or
6
+ * `image/svg+xml` upload would be a stored-XSS page with the project's origin.
7
+ * Types browsers can render harmlessly stay inline; SVG stays inline but is
8
+ * sandboxed (an `<img>` never runs its scripts, and a direct navigation gets an
9
+ * opaque origin); everything else — HTML, XML, scripts, unknown binaries —
10
+ * downloads instead of rendering.
11
+ */
12
+ const INLINE_SAFE_TYPES = /* @__PURE__ */ new Set([
13
+ "image/png",
14
+ "image/jpeg",
15
+ "image/gif",
16
+ "image/webp",
17
+ "image/avif",
18
+ "image/bmp",
19
+ "image/x-icon",
20
+ "image/vnd.microsoft.icon",
21
+ "text/plain",
22
+ "text/csv",
23
+ "application/json",
24
+ "application/pdf"
25
+ ]);
26
+ const INLINE_SAFE_PREFIXES = [
27
+ "video/",
28
+ "audio/",
29
+ "font/"
30
+ ];
31
+ const SANDBOX = "sandbox";
32
+ function blobServingHeaders(contentType, key) {
33
+ const type = contentType.split(";")[0].trim().toLowerCase();
34
+ const headers = {
35
+ "content-type": contentType,
36
+ "x-content-type-options": "nosniff"
37
+ };
38
+ if (type === "image/svg+xml") {
39
+ headers["content-security-policy"] = `default-src 'none'; style-src 'unsafe-inline'; ${SANDBOX}`;
40
+ return headers;
41
+ }
42
+ if (type === "application/pdf") {
43
+ headers["content-security-policy"] = SANDBOX;
44
+ return headers;
45
+ }
46
+ if (INLINE_SAFE_TYPES.has(type) || INLINE_SAFE_PREFIXES.some((p) => type.startsWith(p))) return headers;
47
+ headers["content-disposition"] = `attachment; filename="${(key.split("/").at(-1) ?? "download").replace(/["\r\n\\]/g, "_")}"`;
48
+ headers["content-security-policy"] = SANDBOX;
49
+ return headers;
50
+ }
51
+ //#endregion
2
52
  //#region ../data-plane/src/blob.ts
3
53
  /**
4
54
  * /blob — the SDK's file storage over an injectable BlobStore (a bucket
@@ -118,7 +168,7 @@ function blobRouter(config) {
118
168
  const object = await store.bucket.get(store.prefix + key);
119
169
  if (!object) return c.json({ error: "not_found" }, 404);
120
170
  return new Response(object.body, { headers: {
121
- "content-type": object.httpMetadata?.contentType ?? "application/octet-stream",
171
+ ...blobServingHeaders(object.httpMetadata?.contentType ?? "application/octet-stream", key),
122
172
  "cache-control": "private, no-store"
123
173
  } });
124
174
  });
@@ -270,6 +320,8 @@ const MAX_SQL_ROWS = 1e3;
270
320
  */
271
321
  function readOnlySqlViolation(query) {
272
322
  const bare = query.replace(/--[^\n]*/g, " ").replace(/\/\*[\s\S]*?\*\//g, " ").replace(/'(?:[^']|'')*'/g, "''").replace(/"(?:[^"]|"")*"/g, "\"\"");
323
+ const semicolon = bare.indexOf(";");
324
+ if (semicolon !== -1 && bare.slice(semicolon + 1).trim() !== "") return "multiple statements are not allowed in read-only mode";
273
325
  const first = /^\s*([A-Za-z]+)/.exec(bare)?.[1]?.toUpperCase();
274
326
  if (first === void 0) return "empty statement";
275
327
  if (![
@@ -332,8 +384,21 @@ function dbRouter(config) {
332
384
  error: "invalid_query",
333
385
  detail: "expected { query: string, params?: (string|number|boolean|null)[], readonly?: boolean }"
334
386
  }, 400);
387
+ const verdict = await config.guard?.(c, {
388
+ kind: "sql",
389
+ readonly: body.readonly
390
+ }, resolved);
391
+ if (verdict instanceof Response) return verdict;
392
+ const forced = verdict?.readonly === true;
393
+ if (forced) {
394
+ const violation = readOnlySqlViolation(body.query);
395
+ if (violation) return c.json({
396
+ error: "browser_db_access",
397
+ detail: `${violation}. ${verdict.refusal}`
398
+ }, 403);
399
+ }
335
400
  if (body.readonly === void 0) {
336
- const result = await store.query(body.query, params).catch((err) => err);
401
+ const result = await store.query(body.query, params, forced ? { readonly: true } : void 0).catch((err) => err);
337
402
  if (result instanceof Error) return c.json({
338
403
  error: "sql_error",
339
404
  detail: result.message
@@ -347,7 +412,7 @@ function dbRouter(config) {
347
412
  detail: violation
348
413
  }, 400);
349
414
  }
350
- const result = await store.query(body.query, params, { readonly: body.readonly }).catch((err) => err);
415
+ const result = await store.query(body.query, params, { readonly: body.readonly || forced }).catch((err) => err);
351
416
  if (result instanceof Error) return c.json({
352
417
  error: "sql_error",
353
418
  detail: result.message
@@ -364,6 +429,8 @@ function dbRouter(config) {
364
429
  db.get("/db/schema", async (c) => {
365
430
  const resolved = await resolve(c);
366
431
  if (resolved instanceof Response) return resolved;
432
+ const verdict = await config.guard?.(c, { kind: "schema" }, resolved);
433
+ if (verdict instanceof Response) return verdict;
367
434
  const schema = await introspectSchema(resolved.store, resolved.dialect.name).catch((err) => err);
368
435
  if (schema instanceof Error) return c.json({
369
436
  error: "sql_error",
@@ -377,6 +444,11 @@ function dbRouter(config) {
377
444
  const { store, dialect } = resolved;
378
445
  const collection = c.req.param("collection");
379
446
  if (!COLLECTION_RE.test(collection)) return c.json({ error: "invalid_collection" }, 400);
447
+ const verdict = await config.guard?.(c, {
448
+ kind: "write",
449
+ collection
450
+ }, resolved);
451
+ if (verdict instanceof Response) return verdict;
380
452
  const body = await c.req.json().catch(() => void 0);
381
453
  if (!isPlainObject(body)) return c.json({
382
454
  error: "invalid_document",
@@ -405,6 +477,11 @@ function dbRouter(config) {
405
477
  const { store, dialect } = resolved;
406
478
  const collection = c.req.param("collection");
407
479
  if (!COLLECTION_RE.test(collection)) return c.json({ error: "invalid_collection" }, 400);
480
+ const verdict = await config.guard?.(c, {
481
+ kind: "read",
482
+ collection
483
+ }, resolved);
484
+ if (verdict instanceof Response) return verdict;
408
485
  const q = c.req.query();
409
486
  try {
410
487
  const limit = intParam(q.limit, "limit", 100, MAX_LIMIT);
@@ -457,6 +534,11 @@ function dbRouter(config) {
457
534
  const { store, dialect } = resolved;
458
535
  const collection = c.req.param("collection");
459
536
  if (!COLLECTION_RE.test(collection)) return c.json({ error: "invalid_collection" }, 400);
537
+ const verdict = await config.guard?.(c, {
538
+ kind: "read",
539
+ collection
540
+ }, resolved);
541
+ if (verdict instanceof Response) return verdict;
460
542
  const get = dialect.get(collection, c.req.param("id"));
461
543
  const row = (await store.query(get.sql, get.params)).results[0];
462
544
  return c.json(row ? toDoc(row) : null);
@@ -467,6 +549,11 @@ function dbRouter(config) {
467
549
  const { store, dialect } = resolved;
468
550
  const collection = c.req.param("collection");
469
551
  if (!COLLECTION_RE.test(collection)) return c.json({ error: "invalid_collection" }, 400);
552
+ const verdict = await config.guard?.(c, {
553
+ kind: "write",
554
+ collection
555
+ }, resolved);
556
+ if (verdict instanceof Response) return verdict;
470
557
  const body = await c.req.json().catch(() => void 0);
471
558
  if (!isPlainObject(body)) return c.json({
472
559
  error: "invalid_patch",
@@ -483,6 +570,11 @@ function dbRouter(config) {
483
570
  const { store, dialect } = resolved;
484
571
  const collection = c.req.param("collection");
485
572
  if (!COLLECTION_RE.test(collection)) return c.json({ error: "invalid_collection" }, 400);
573
+ const verdict = await config.guard?.(c, {
574
+ kind: "write",
575
+ collection
576
+ }, resolved);
577
+ if (verdict instanceof Response) return verdict;
486
578
  const del = dialect.del(collection, c.req.param("id"));
487
579
  const result = await store.query(del.sql, del.params);
488
580
  return c.json({ deleted: (result.meta.changes ?? 0) > 0 });
@@ -1,5 +1,5 @@
1
- import { c as resolveProject, l as stagingUrl, o as projectFlagArgs, s as projectUrl } from "./site-BFaWfSVu.mjs";
2
- import { a as unwrap, d as withContext, i as requireTty, l as apiRequest, r as reportErrors, t as formatDateTime } from "./ui--atbZ-TM.mjs";
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";
3
3
  import { defineCommand } from "citty";
4
4
  import * as p from "@clack/prompts";
5
5
  //#region src/staging.ts
@@ -1,5 +1,5 @@
1
- import { a as projectDirArgs, c as resolveProject, o as projectFlagArgs } from "./site-BFaWfSVu.mjs";
2
- import { l as apiRequest, r as reportErrors, t as formatDateTime } from "./ui--atbZ-TM.mjs";
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";
3
3
  import { defineCommand } from "citty";
4
4
  const tokenCommand = defineCommand({
5
5
  meta: {
@@ -21,9 +21,10 @@ const tokenCommand = defineCommand({
21
21
  },
22
22
  run: reportErrors("token create", async ({ args }) => {
23
23
  const { slug } = resolveProject(args);
24
- const { token } = await apiRequest("POST", `/projects/${slug}/deploy-tokens`, { name: args.name });
24
+ const { token, idle_days } = await apiRequest("POST", `/projects/${slug}/deploy-tokens`, { name: args.name });
25
25
  console.log(`Deploy token for ${slug} (shown once — store it now):\n`);
26
26
  console.log(` ${token}\n`);
27
+ console.log(`It belongs to the project and stops working after ${idle_days} days without a deploy.`);
27
28
  console.log("Add it to the GitHub repo as secret CANOPY_DEPLOY_TOKEN");
28
29
  console.log("(gh secret set CANOPY_DEPLOY_TOKEN, or repo Settings → Secrets).");
29
30
  console.log("Pushes to main then deploy via .github/workflows/canopy-deploy.yml.");
@@ -42,7 +43,10 @@ const tokenCommand = defineCommand({
42
43
  console.log(`No deploy tokens for ${slug} — create one with \`canopy token create\`.`);
43
44
  return;
44
45
  }
45
- for (const t of tokens) console.log(`${t.id} ${t.name ?? "(unnamed)"} created ${formatDateTime(t.created_at)} by ${t.created_by ?? "?"}`);
46
+ for (const t of tokens) {
47
+ const used = t.last_used_at ? `last used ${formatDateTime(t.last_used_at)}` : "never used";
48
+ console.log(`${t.id} ${t.name ?? "(unnamed)"} created ${formatDateTime(t.created_at)} by ${t.created_by ?? "?"} ${used}`);
49
+ }
46
50
  })
47
51
  }),
48
52
  revoke: defineCommand({
@@ -1,4 +1,4 @@
1
- import { n as siteConfig } from "./site-BFaWfSVu.mjs";
1
+ import { n as siteConfig } from "./site-DM2J10CB.mjs";
2
2
  import { spawn } from "node:child_process";
3
3
  import { cancel, isCancel } from "@clack/prompts";
4
4
  //#region src/api.ts
@@ -1,5 +1,5 @@
1
1
  import { t as PACKAGE_NAME } from "./version-check-CcsA1QjI.mjs";
2
- import { r as reportErrors } from "./ui--atbZ-TM.mjs";
2
+ import { r as reportErrors } from "./ui-CUNkis89.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-BvoYCIKk.mjs");
115
+ const { skillInstalled } = await import("./skill-Dt49jdUT.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
- import { c as resolveProject, o as projectFlagArgs } from "./site-BFaWfSVu.mjs";
2
- import { d as withContext, l as apiRequest, r as reportErrors } from "./ui--atbZ-TM.mjs";
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";
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.22",
3
+ "version": "0.7.24",
4
4
  "description": "Canopy CLI — create, develop and deploy Canopy projects.",
5
5
  "bin": {
6
6
  "canopy": "./dist/index.mjs"
@@ -20,8 +20,8 @@
20
20
  "esbuild": "^0.28.1",
21
21
  "hono": "^4.8.0",
22
22
  "vite-server": "npm:vite@^7",
23
- "@mediamonks/canopy-data-plane": "0.0.4",
24
- "@mediamonks/canopy": "0.0.6"
23
+ "@mediamonks/canopy": "0.0.7",
24
+ "@mediamonks/canopy-data-plane": "0.0.4"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^26.1.1",
@@ -1,204 +0,0 @@
1
- import { c as resolveProject, o as projectFlagArgs } from "./site-BFaWfSVu.mjs";
2
- import { a as unwrap, d as withContext, i as requireTty, r as reportErrors, s as apiDataRequest } from "./ui--atbZ-TM.mjs";
3
- import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-Bg9FEGFh.mjs";
4
- import { defineCommand } from "citty";
5
- import { readFileSync, writeFileSync } from "node:fs";
6
- import * as p from "@clack/prompts";
7
- //#region src/db.ts
8
- /**
9
- * canopy db pull | snapshot | snapshots | restore — data safety from the
10
- * terminal (docs/04-roadmap.md Phase 2 item 4, pulled forward).
11
- *
12
- * pull download a fresh SQL dump of the project database
13
- * snapshot store a dump server-side (restorable from CLI or dashboard)
14
- * snapshots list stored snapshots
15
- * restore replace ALL project data from a snapshot or a local .sql file
16
- * migrate apply pending schema migrations (docs/13-migrations-and-drizzle.md)
17
- *
18
- * The project comes from canopy.json in the current directory, or
19
- * --project <slug>.
20
- */
21
- /** Shared flag: operate on the staging channel's database (docs/14). */
22
- const stagingFlagArgs = { staging: {
23
- type: "boolean",
24
- default: false,
25
- description: "Target the staging channel's database instead of live"
26
- } };
27
- const channelOf = (args) => args.staging ? "staging" : "live";
28
- const channelNote = (args) => args.staging ? " [staging]" : "";
29
- const formatSize = (bytes) => bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(1)} kB` : `${(bytes / 1024 / 1024).toFixed(1)} MB`;
30
- const dbCommand = defineCommand({
31
- meta: {
32
- name: "db",
33
- description: "Data tools: pull, snapshot, snapshots, restore, migrate"
34
- },
35
- subCommands: {
36
- pull: defineCommand({
37
- meta: {
38
- name: "pull",
39
- description: "Download a fresh SQL dump of the project database"
40
- },
41
- args: {
42
- ...projectFlagArgs,
43
- ...stagingFlagArgs,
44
- out: {
45
- type: "string",
46
- description: "Output file (defaults to <slug>-<timestamp>.sql)"
47
- }
48
- },
49
- run: reportErrors("db pull", async ({ args }) => {
50
- const { slug } = resolveProject(args);
51
- const out = args.out ?? `${slug}-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replaceAll(/[:T]/g, "-")}.sql`;
52
- const spin = p.spinner();
53
- spin.start(`Exporting ${slug}${channelNote(args)}…`);
54
- try {
55
- const { url } = await apiDataRequest("POST", "/db/export", slug, void 0, void 0, channelOf(args));
56
- const download = await fetch(url);
57
- if (!download.ok) throw new Error(`Download failed (HTTP ${download.status}).`);
58
- const sql = Buffer.from(await download.arrayBuffer());
59
- writeFileSync(out, sql);
60
- spin.stop(`Wrote ${out} (${formatSize(sql.byteLength)}).`);
61
- } catch (err) {
62
- spin.error("Export failed.");
63
- throw err;
64
- }
65
- })
66
- }),
67
- snapshot: defineCommand({
68
- meta: {
69
- name: "snapshot",
70
- description: "Store a dump server-side (restorable anytime)"
71
- },
72
- args: {
73
- ...projectFlagArgs,
74
- ...stagingFlagArgs
75
- },
76
- run: reportErrors("db snapshot", async ({ args }) => {
77
- const { slug } = resolveProject(args);
78
- const spin = p.spinner();
79
- spin.start(`Snapshotting ${slug}${channelNote(args)}…`);
80
- let created;
81
- try {
82
- ({snapshot: created} = await apiDataRequest("POST", "/db/snapshots", slug, void 0, void 0, channelOf(args)));
83
- } catch (err) {
84
- spin.error("Snapshot failed.");
85
- throw err;
86
- }
87
- spin.stop(`Stored ${created.key} (${formatSize(created.size)}).`);
88
- console.log(`Restore anytime with \`canopy db restore --snapshot ${created.key}\`.`);
89
- })
90
- }),
91
- snapshots: defineCommand({
92
- meta: {
93
- name: "snapshots",
94
- description: "List stored snapshots"
95
- },
96
- args: {
97
- ...projectFlagArgs,
98
- ...stagingFlagArgs
99
- },
100
- run: reportErrors("db snapshots", async ({ args }) => {
101
- const { slug } = resolveProject(args);
102
- const { snapshots } = await apiDataRequest("GET", "/db/snapshots", slug, void 0, void 0, channelOf(args));
103
- if (snapshots.length === 0) {
104
- console.log(`No snapshots for ${slug} yet — run \`canopy db snapshot\`.`);
105
- return;
106
- }
107
- for (const snapshot of snapshots) console.log(`${snapshot.key} ${formatSize(snapshot.size).padStart(9)} ${snapshot.createdAt}`);
108
- })
109
- }),
110
- restore: defineCommand({
111
- meta: {
112
- name: "restore",
113
- description: "Replace ALL project data from a snapshot or a local .sql file"
114
- },
115
- args: {
116
- ...projectFlagArgs,
117
- ...stagingFlagArgs,
118
- file: {
119
- type: "positional",
120
- description: "Local .sql dump to restore from",
121
- required: false
122
- },
123
- snapshot: {
124
- type: "string",
125
- description: "Server-side snapshot key to restore from"
126
- },
127
- yes: {
128
- type: "boolean",
129
- default: false,
130
- description: "Skip the type-the-slug confirmation"
131
- }
132
- },
133
- run: reportErrors("db restore", async ({ args }) => {
134
- const { slug } = resolveProject(args);
135
- const { file } = args;
136
- let snapshotKey = args.snapshot;
137
- if (!snapshotKey && !file) {
138
- requireTty("pass a local .sql dump or --snapshot <key>.");
139
- const { snapshots } = await apiDataRequest("GET", "/db/snapshots", slug, void 0, void 0, channelOf(args));
140
- if (snapshots.length === 0) throw new Error(`No snapshots for ${slug} — run \`canopy db snapshot\` first, or pass a local .sql dump.`);
141
- snapshotKey = unwrap(await p.select({
142
- message: `Snapshot to restore into ${slug}`,
143
- options: snapshots.map((snapshot) => ({
144
- value: snapshot.key,
145
- label: snapshot.key,
146
- hint: `${formatSize(snapshot.size)} · ${snapshot.createdAt}`
147
- }))
148
- }));
149
- }
150
- if (!args.yes) {
151
- requireTty("pass --yes to restore without confirmation.");
152
- if (unwrap(await p.text({ message: `Restore will REPLACE all current data in "${slug}"${channelNote(args)} with ${snapshotKey ?? file}. Type the slug to confirm.` })).trim() !== slug) {
153
- p.cancel("Aborted.");
154
- return;
155
- }
156
- }
157
- const restoreContext = { action: `restore "${slug}" from ${snapshotKey ?? file}` };
158
- if (snapshotKey) await withContext(apiDataRequest("POST", "/db/restore", slug, { snapshot: snapshotKey }, void 0, channelOf(args)), restoreContext);
159
- else if (file) await withContext(apiDataRequest("POST", "/db/restore", slug, readFileSync(file, "utf8"), "application/sql", channelOf(args)), restoreContext);
160
- p.log.success(`Restored ${slug}${channelNote(args)}.`);
161
- })
162
- }),
163
- migrate: defineCommand({
164
- meta: {
165
- name: "migrate",
166
- description: "Apply pending schema migrations to the project database"
167
- },
168
- args: {
169
- ...projectFlagArgs,
170
- ...stagingFlagArgs,
171
- plan: {
172
- type: "boolean",
173
- default: false,
174
- description: "Dry run: list pending migrations without applying anything"
175
- }
176
- },
177
- run: reportErrors("db migrate", async ({ args }) => {
178
- const { dir, slug } = resolveProject(args);
179
- const files = readMigrationFiles(dir);
180
- if (files === void 0) throw new Error("No migrations directory configured — set \"migrations\" in canopy.json (e.g. {\"migrations\": \"./migrations\"}, pointing at your drizzle-kit output).");
181
- if (args.plan) {
182
- const result = await withContext(migrateProject(slug, files, true, channelOf(args)), { action: `plan migrations for "${slug}"${channelNote(args)}` });
183
- if (result.pending.length === 0) console.log(`Nothing pending (${result.applied.length} applied previously).`);
184
- else for (const name of result.pending) console.log(`pending ${name}`);
185
- for (const line of describeMigrateResult(result)) console.log(line);
186
- return;
187
- }
188
- const spin = p.spinner();
189
- spin.start(`Migrating ${slug}${channelNote(args)}…`);
190
- let result;
191
- try {
192
- result = await withContext(migrateProject(slug, files, false, channelOf(args)), { action: `migrate "${slug}"${channelNote(args)}` });
193
- } catch (err) {
194
- spin.error("Migrate failed.");
195
- throw err;
196
- }
197
- spin.stop(result.appliedNow.length === 0 ? `Nothing pending for ${slug}.` : `Applied ${result.appliedNow.length} migration(s) to ${slug}.`);
198
- for (const line of describeMigrateResult(result)) console.log(` ${line}`);
199
- })
200
- })
201
- }
202
- });
203
- //#endregion
204
- export { dbCommand };
@@ -1,2 +0,0 @@
1
- import { t as devCommand } from "./dev-CNqx_53I.mjs";
2
- export { devCommand };