@mediamonks/canopy-cli 0.5.0 → 0.6.0

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 (39) hide show
  1. package/dist/{db-BW1ZolWn.mjs → db-BpnjQUkF.mjs} +42 -3
  2. package/dist/{deploy-alC2--pZ.mjs → deploy-DdSSL227.mjs} +29 -2
  3. package/dist/dev-DV5ud7AJ.mjs +2 -0
  4. package/dist/{dev-CQQhMOoD.mjs → dev-OxeYndd1.mjs} +24 -9
  5. package/dist/docs/ci.md +1 -3
  6. package/dist/docs/cli.md +3 -4
  7. package/dist/docs/data.md +5 -4
  8. package/dist/docs/deploy.md +1 -3
  9. package/dist/docs/dev.md +13 -11
  10. package/dist/docs/getting-started.md +1 -3
  11. package/dist/docs/infrastructure.md +27 -15
  12. package/dist/docs/migrations.md +166 -0
  13. package/dist/docs/porting-nextjs.md +232 -0
  14. package/dist/docs/sdk-ai.md +103 -0
  15. package/dist/docs/sdk-auth.md +57 -0
  16. package/dist/docs/sdk-database.md +126 -0
  17. package/dist/docs/sdk-files.md +82 -0
  18. package/dist/docs/sdk-services.md +58 -0
  19. package/dist/docs/sdk.md +113 -0
  20. package/dist/docs/static-serving.md +4 -1
  21. package/dist/docs/troubleshooting.md +1 -3
  22. package/dist/{docs-DvSPpSIR.mjs → docs-Cept8t_3.mjs} +23 -10
  23. package/dist/index.mjs +20 -20
  24. package/dist/{local-C5-GWJcx.mjs → local-D9OVEHDg.mjs} +363 -39
  25. package/dist/{login-Bz-rwlFF.mjs → login-Cb8D8a1Z.mjs} +125 -4
  26. package/dist/migrations-B0kJarjr.mjs +50 -0
  27. package/dist/{open-DnlYDqrU.mjs → open-BmgYBSdR.mjs} +2 -2
  28. package/dist/{package-Bi5FfVSP.mjs → package-D1wDsLSU.mjs} +1 -1
  29. package/dist/{projects-ijfe7Ffx.mjs → projects-CoGh8m9A.mjs} +17 -9
  30. package/dist/{site-BJjoePQS.mjs → site-DQnysZ3W.mjs} +8 -1
  31. package/dist/skill/canopy/SKILL.md +18 -1
  32. package/dist/{skill-IAAa2EkK.mjs → skill-192EdixC.mjs} +2 -2
  33. package/dist/templates/worker/AGENTS.md +3 -0
  34. package/dist/{token-BeF9zczg.mjs → token-CWgp_Lqe.mjs} +2 -2
  35. package/dist/{ui-DThlyHgo.mjs → ui-BhzMeKQl.mjs} +1 -1
  36. package/dist/{upgrade-DNxR-zh7.mjs → upgrade-BrHSSg2e.mjs} +2 -2
  37. package/dist/{usage-DQpH7A3X.mjs → usage-CYQRLXwf.mjs} +2 -2
  38. package/package.json +3 -3
  39. package/dist/dev-BJ5xEwqW.mjs +0 -2
@@ -0,0 +1,50 @@
1
+ import { l as tryReadCanopyJson } from "./site-DQnysZ3W.mjs";
2
+ import { o as apiDataRequest } from "./ui-BhzMeKQl.mjs";
3
+ import { readFileSync, readdirSync } from "node:fs";
4
+ import { join, resolve } from "node:path";
5
+ //#region src/migrations.ts
6
+ /**
7
+ * The CLI side of schema migrations (docs/13-migrations-and-drizzle.md):
8
+ * reads the project's migration files (canopy.json "migrations" dir, e.g.
9
+ * Drizzle's `drizzle-kit generate` output) and sends them to the platform's
10
+ * migrate primitive. Shared by `canopy db migrate`, the deploy step, and the
11
+ * `--local` sandbox boot.
12
+ */
13
+ /**
14
+ * The project's migration files in apply order, or undefined when canopy.json
15
+ * configures no "migrations" dir. A configured-but-unreadable dir is an error;
16
+ * a readable dir with no .sql files is an empty list.
17
+ */
18
+ function readMigrationFiles(dir) {
19
+ const migrationsPath = tryReadCanopyJson(dir)?.migrations;
20
+ if (!migrationsPath) return void 0;
21
+ const migrationsDir = resolve(dir, migrationsPath);
22
+ let entries;
23
+ try {
24
+ entries = readdirSync(migrationsDir, { withFileTypes: true });
25
+ } catch {
26
+ throw new Error(`Migrations directory not found: ${migrationsDir} (canopy.json "migrations").`);
27
+ }
28
+ return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".sql")).map((entry) => ({
29
+ name: entry.name,
30
+ sql: readFileSync(join(migrationsDir, entry.name), "utf8")
31
+ })).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
32
+ }
33
+ /** Run (or dry-run) the project's migrations against its live database. */
34
+ function migrateProject(slug, migrations, plan = false) {
35
+ return apiDataRequest("POST", "/db/migrate", slug, {
36
+ migrations,
37
+ plan
38
+ });
39
+ }
40
+ /** One-line-per-fact summary shared by `db migrate` and the deploy step. */
41
+ function describeMigrateResult(result) {
42
+ const lines = [];
43
+ for (const name of result.appliedNow) lines.push(`applied ${name}`);
44
+ if (result.appliedNow.length === 0 && result.pending.length === 0) lines.push(`nothing pending (${result.applied.length} applied previously)`);
45
+ if (result.snapshot !== void 0) lines.push(`pre-migrate snapshot: ${result.snapshot} (canopy db restore --snapshot …)`);
46
+ if (result.missing.length > 0) lines.push(`note: ${result.missing.length} journaled migration(s) have no local file`);
47
+ return lines;
48
+ }
49
+ //#endregion
50
+ export { migrateProject as n, readMigrationFiles as r, describeMigrateResult as t };
@@ -1,5 +1,5 @@
1
- import { i as projectDirArgs, o as projectUrl, s as resolveProject } from "./site-BJjoePQS.mjs";
2
- import { n as reportErrors, t as openBrowser } from "./ui-DThlyHgo.mjs";
1
+ import { a as projectDirArgs, c as resolveProject, s as projectUrl } from "./site-DQnysZ3W.mjs";
2
+ import { n as reportErrors, t as openBrowser } from "./ui-BhzMeKQl.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  //#region src/open.ts
5
5
  /**
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.5.0";
2
+ var version = "0.6.0";
3
3
  //#endregion
4
4
  export { version as t };
@@ -1,5 +1,5 @@
1
- import { c as tryReadCanopyJson, n as siteConfig, o as projectUrl } from "./site-BJjoePQS.mjs";
2
- import { c as apiRequest, i as unwrap, n as reportErrors, r as requireTty } from "./ui-DThlyHgo.mjs";
1
+ import { l as tryReadCanopyJson, n as siteConfig, s as projectUrl } from "./site-DQnysZ3W.mjs";
2
+ import { c as apiRequest, i as unwrap, n as reportErrors, r as requireTty } from "./ui-BhzMeKQl.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
5
5
  import { join, relative, resolve, sep } from "node:path";
@@ -75,7 +75,12 @@ function writeCanopyJson(dir, fields) {
75
75
  writeFileSync(join(dir, "canopy.json"), `${JSON.stringify(next, null, 2)}\n`);
76
76
  }
77
77
  function slugify(input) {
78
- return input.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/(^-+|-+$)/g, "").slice(0, 40);
78
+ let slug = input.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/(^-+|-+$)/g, "").slice(0, 40);
79
+ while (slug.includes("-dot-")) slug = slug.replaceAll("-dot-", "-");
80
+ return slug;
81
+ }
82
+ function shouldScaffoldCreate(requested, cwd = process.cwd()) {
83
+ return requested ?? !existsSync(join(cwd, "canopy.json"));
79
84
  }
80
85
  const createCommand = defineCommand({
81
86
  meta: {
@@ -98,7 +103,6 @@ const createCommand = defineCommand({
98
103
  },
99
104
  scaffold: {
100
105
  type: "boolean",
101
- default: true,
102
106
  description: "Scaffold ./<slug>/ from the starter template",
103
107
  negativeDescription: "Only register — no files written"
104
108
  }
@@ -118,12 +122,16 @@ const createCommand = defineCommand({
118
122
  message: "Slug",
119
123
  defaultValue: slugify(name),
120
124
  placeholder: slugify(name),
121
- validate: (value) => !value || /^[a-z0-9][a-z0-9-]*$/.test(value) ? void 0 : "Lowercase letters, digits and dashes only."
125
+ validate: (value) => {
126
+ if (value && !/^[a-z0-9][a-z0-9-]*$/.test(value)) return "Lowercase letters, digits and dashes only.";
127
+ if (value?.includes("-dot-")) return `"-dot-" is reserved for preview hostnames.`;
128
+ }
122
129
  })).trim();
123
130
  slug ||= slugify(name);
131
+ const scaffold = shouldScaffoldCreate(args.scaffold);
124
132
  let template = TEMPLATES.find((t) => t.id === (args.template ?? "static"));
125
133
  if (!template) throw new Error(`Unknown template "${args.template}" — available: ${TEMPLATES.map((t) => t.id).join(", ")}.`);
126
- if (args.scaffold && !args.template && process.stdin.isTTY) {
134
+ if (scaffold && !args.template && process.stdin.isTTY) {
127
135
  const id = unwrap(await p.select({
128
136
  message: "Template",
129
137
  options: TEMPLATES.filter((t) => !t.hidden).map((t) => ({
@@ -136,7 +144,7 @@ const createCommand = defineCommand({
136
144
  }
137
145
  if (template.hidden) p.log.warn("Per-project Workers are not deployable yet (Phase 2) — `canopy deploy` publishes the frontend only for now.");
138
146
  const targetDir = resolve(slug);
139
- if (args.scaffold && existsSync(targetDir)) throw new Error(`Directory ./${slug} already exists — remove it, or use --no-scaffold to only register.`);
147
+ if (scaffold && existsSync(targetDir)) throw new Error(`Directory ./${slug} already exists — remove it, or use --no-scaffold to only register.`);
140
148
  const spin = p.spinner();
141
149
  spin.start(`Registering ${slug}…`);
142
150
  let project;
@@ -150,7 +158,7 @@ const createCommand = defineCommand({
150
158
  throw err;
151
159
  }
152
160
  spin.stop(`Created ${project.name} — ${projectUrl(project.slug)}`);
153
- if (args.scaffold) {
161
+ if (scaffold) {
154
162
  scaffoldTemplate(template.id, targetDir, {
155
163
  name,
156
164
  slug
@@ -166,7 +174,7 @@ const createCommand = defineCommand({
166
174
  ];
167
175
  p.note(steps.join("\n"), `Scaffolded ./${slug}/ (${template.id}) — next`);
168
176
  p.outro(`Live URL (after first deploy): ${projectUrl(project.slug)}`);
169
- } else p.outro("Registered without scaffolding — add a canopy.json to deploy an existing app.");
177
+ } else p.outro(existsSync(join(process.cwd(), "canopy.json")) ? "Registered without scaffolding — the existing canopy.json was left unchanged." : "Registered without scaffolding — add a canopy.json to deploy an existing app.");
170
178
  })
171
179
  });
172
180
  /** Empty enough to scaffold into: missing, or holding only dotfiles (.git, .agents…). */
@@ -185,6 +185,13 @@ function tryReadCanopyJson(dir) {
185
185
  }
186
186
  return JSON.parse(raw);
187
187
  }
188
+ /** The validated dialect lock from canopy.json, if the project set one. */
189
+ function projectDialect(dir) {
190
+ const dialect = tryReadCanopyJson(dir)?.dialect;
191
+ if (dialect === void 0) return void 0;
192
+ if (dialect !== "sqlite" && dialect !== "postgres") throw new Error(`canopy.json "dialect" must be "sqlite" or "postgres", not ${JSON.stringify(dialect)}.`);
193
+ return dialect;
194
+ }
188
195
  function readCanopyJson(dir) {
189
196
  const config = tryReadCanopyJson(dir);
190
197
  if (config === void 0) throw new Error(`No canopy.json in ${dir} — run from a project created by \`canopy create\`, add one ({"slug": "<slug>", "dist": "dist"}), or pass --project <slug>.`);
@@ -251,4 +258,4 @@ function siteHeaderLine(cliVersion) {
251
258
  return `canopy ${cliVersion} · ${c.envOverride || !c.profileId ? c.apiUrl : `${c.profileId} (${c.projectDomain ?? new URL(c.apiUrl).hostname})`}`;
252
259
  }
253
260
  //#endregion
254
- export { projectFlagArgs as a, tryReadCanopyJson as c, readConfigFile as d, setActiveProfile as f, isProfileId as g, getBakedProfile as h, projectDirArgs as i, clearAllProfileTokens as l, writeConfigFile as m, siteConfig as n, projectUrl as o, upsertProfileAuth as p, siteHeaderLine as r, resolveProject as s, setInvocationProfile as t, listBakedProfiles as u };
261
+ export { isProfileId as _, projectDirArgs as a, resolveProject as c, listBakedProfiles as d, readConfigFile as f, getBakedProfile as g, writeConfigFile as h, projectDialect as i, tryReadCanopyJson as l, upsertProfileAuth as m, siteConfig as n, projectFlagArgs as o, setActiveProfile as p, siteHeaderLine as r, projectUrl as s, setInvocationProfile as t, clearAllProfileTokens as u };
@@ -91,9 +91,26 @@ truth and never go stale.
91
91
  - Deploy: `canopy deploy` (add `--build` if the project has a build step)
92
92
  - Roll back: `canopy rollback`
93
93
  - Open the live site: `canopy open`
94
- - Manage data: `canopy db pull | snapshot | snapshots | restore`
94
+ - Manage data: `canopy db pull | snapshot | snapshots | restore | migrate`
95
95
  - CI credentials: `canopy token create | list | revoke`
96
96
 
97
+ ## Custom tables & migrations (devs; Drizzle recommended)
98
+
99
+ The document API (`canopy.db.collection(…)`) needs no schema — prefer it. When
100
+ a project defines its **own tables** (raw `canopy.db.sql()` / Drizzle), use the
101
+ migrations flow — read `canopy docs migrations` first. The rules that prevent
102
+ confusing failures:
103
+
104
+ - Pin `"dialect"` and `"migrations"` in `canopy.json`; put ordered `.sql`
105
+ files in that directory (Drizzle: `npx drizzle-kit generate` writes them).
106
+ - **There is no `DATABASE_URL` and never will be.** Never run
107
+ `drizzle-kit push`/`migrate`/`studio` or Drizzle's runtime `migrate()` —
108
+ generating is local, applying is Canopy's job: `canopy deploy` applies
109
+ pending migrations automatically (`canopy db migrate` applies without a
110
+ deploy; `canopy dev --local` applies to the sandbox on boot).
111
+ - Never edit an applied migration (the platform refuses edited history) and
112
+ never reference `_canopy_*` tables.
113
+
97
114
  ## Choosing a starter template (`canopy create` / `canopy init`)
98
115
 
99
116
  Both `canopy create` and `canopy init` scaffold from the same three templates.
@@ -1,5 +1,5 @@
1
- import { t as version } from "./package-Bi5FfVSP.mjs";
2
- import { n as reportErrors } from "./ui-DThlyHgo.mjs";
1
+ import { t as version } from "./package-D1wDsLSU.mjs";
2
+ import { n as reportErrors } from "./ui-BhzMeKQl.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import { cpSync, existsSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
5
5
  import { join, relative } from "node:path";
@@ -39,6 +39,9 @@ the [Canopy dashboard](https://dashboard.thanarie.dev).
39
39
  writes never reach the platform. Prefer it for destructive experiments.
40
40
  - `canopy deploy` — builds (`npm run build`) and publishes; live (behind
41
41
  company login) at https://{{slug}}.thanarie.dev.
42
+ - Defining your own tables (raw `sql()` / Drizzle)? Use the migrations flow —
43
+ `canopy docs migrations`. Never invent a `DATABASE_URL` (none exists):
44
+ generate migrations locally, and `canopy deploy` applies them.
42
45
 
43
46
  ## Boundaries
44
47
 
@@ -1,5 +1,5 @@
1
- import { a as projectFlagArgs, i as projectDirArgs, s as resolveProject } from "./site-BJjoePQS.mjs";
2
- import { c as apiRequest, n as reportErrors } from "./ui-DThlyHgo.mjs";
1
+ import { a as projectDirArgs, c as resolveProject, o as projectFlagArgs } from "./site-DQnysZ3W.mjs";
2
+ import { c as apiRequest, n as reportErrors } from "./ui-BhzMeKQl.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  const tokenCommand = defineCommand({
5
5
  meta: {
@@ -1,4 +1,4 @@
1
- import { n as siteConfig } from "./site-BJjoePQS.mjs";
1
+ import { n as siteConfig } from "./site-DQnysZ3W.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-Cuh6HaLX.mjs";
2
- import { n as reportErrors } from "./ui-DThlyHgo.mjs";
2
+ import { n as reportErrors } from "./ui-BhzMeKQl.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-IAAa2EkK.mjs");
115
+ const { skillInstalled } = await import("./skill-192EdixC.mjs");
116
116
  const refreshSkill = skillInstalled(process.cwd());
117
117
  console.log(`Upgrading via ${command}: ${command} ${cmd.join(" ")}`);
118
118
  await run(command, cmd);
@@ -1,5 +1,5 @@
1
- import { a as projectFlagArgs, s as resolveProject } from "./site-BJjoePQS.mjs";
2
- import { c as apiRequest, l as withContext, n as reportErrors } from "./ui-DThlyHgo.mjs";
1
+ import { c as resolveProject, o as projectFlagArgs } from "./site-DQnysZ3W.mjs";
2
+ import { c as apiRequest, l as withContext, n as reportErrors } from "./ui-BhzMeKQl.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.5.0",
3
+ "version": "0.6.0",
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": "0.0.3",
24
- "@mediamonks/canopy-data-plane": "0.0.1"
23
+ "@mediamonks/canopy-data-plane": "0.0.2",
24
+ "@mediamonks/canopy": "0.0.3"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^26.1.1",
@@ -1,2 +0,0 @@
1
- import { t as devCommand } from "./dev-CQQhMOoD.mjs";
2
- export { devCommand };