@mediamonks/canopy-cli 0.5.1 → 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.
- package/dist/{db-BW1ZolWn.mjs → db-BpnjQUkF.mjs} +42 -3
- package/dist/{deploy-alC2--pZ.mjs → deploy-DdSSL227.mjs} +29 -2
- package/dist/dev-DV5ud7AJ.mjs +2 -0
- package/dist/{dev-CQQhMOoD.mjs → dev-OxeYndd1.mjs} +24 -9
- package/dist/docs/ci.md +1 -3
- package/dist/docs/cli.md +3 -4
- package/dist/docs/data.md +5 -4
- package/dist/docs/deploy.md +1 -3
- package/dist/docs/dev.md +13 -11
- package/dist/docs/getting-started.md +1 -3
- package/dist/docs/infrastructure.md +27 -15
- package/dist/docs/migrations.md +166 -0
- package/dist/docs/porting-nextjs.md +232 -0
- package/dist/docs/sdk-ai.md +103 -0
- package/dist/docs/sdk-auth.md +57 -0
- package/dist/docs/sdk-database.md +126 -0
- package/dist/docs/sdk-files.md +82 -0
- package/dist/docs/sdk-services.md +58 -0
- package/dist/docs/sdk.md +113 -0
- package/dist/docs/static-serving.md +4 -1
- package/dist/docs/troubleshooting.md +1 -3
- package/dist/{docs-DvSPpSIR.mjs → docs-Cept8t_3.mjs} +23 -10
- package/dist/index.mjs +20 -20
- package/dist/{local-C5-GWJcx.mjs → local-D9OVEHDg.mjs} +363 -39
- package/dist/{login-BO6abf40.mjs → login-Cb8D8a1Z.mjs} +5 -3
- package/dist/migrations-B0kJarjr.mjs +50 -0
- package/dist/{open-DnlYDqrU.mjs → open-BmgYBSdR.mjs} +2 -2
- package/dist/{package-CHoZKzaw.mjs → package-D1wDsLSU.mjs} +1 -1
- package/dist/{projects-ijfe7Ffx.mjs → projects-CoGh8m9A.mjs} +17 -9
- package/dist/{site-BJjoePQS.mjs → site-DQnysZ3W.mjs} +8 -1
- package/dist/skill/canopy/SKILL.md +18 -1
- package/dist/{skill-CW0QSbAJ.mjs → skill-192EdixC.mjs} +2 -2
- package/dist/templates/worker/AGENTS.md +3 -0
- package/dist/{token-BeF9zczg.mjs → token-CWgp_Lqe.mjs} +2 -2
- package/dist/{ui-DThlyHgo.mjs → ui-BhzMeKQl.mjs} +1 -1
- package/dist/{upgrade-CGz1yzsW.mjs → upgrade-BrHSSg2e.mjs} +2 -2
- package/dist/{usage-DQpH7A3X.mjs → usage-CYQRLXwf.mjs} +2 -2
- package/package.json +3 -3
- package/dist/dev-BJ5xEwqW.mjs +0 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { i as unwrap, l as withContext, n as reportErrors, o as apiDataRequest, r as requireTty } from "./ui-
|
|
1
|
+
import { c as resolveProject, o as projectFlagArgs } from "./site-DQnysZ3W.mjs";
|
|
2
|
+
import { i as unwrap, l as withContext, n as reportErrors, o as apiDataRequest, r as requireTty } from "./ui-BhzMeKQl.mjs";
|
|
3
|
+
import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-B0kJarjr.mjs";
|
|
3
4
|
import { defineCommand } from "citty";
|
|
4
5
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
5
6
|
import * as p from "@clack/prompts";
|
|
@@ -12,6 +13,7 @@ import * as p from "@clack/prompts";
|
|
|
12
13
|
* snapshot store a dump server-side (restorable from CLI or dashboard)
|
|
13
14
|
* snapshots list stored snapshots
|
|
14
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)
|
|
15
17
|
*
|
|
16
18
|
* The project comes from canopy.json in the current directory, or
|
|
17
19
|
* --project <slug>.
|
|
@@ -20,7 +22,7 @@ const formatSize = (bytes) => bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(1)
|
|
|
20
22
|
const dbCommand = defineCommand({
|
|
21
23
|
meta: {
|
|
22
24
|
name: "db",
|
|
23
|
-
description: "Data tools: pull, snapshot, snapshots, restore"
|
|
25
|
+
description: "Data tools: pull, snapshot, snapshots, restore, migrate"
|
|
24
26
|
},
|
|
25
27
|
subCommands: {
|
|
26
28
|
pull: defineCommand({
|
|
@@ -141,6 +143,43 @@ const dbCommand = defineCommand({
|
|
|
141
143
|
else if (file) await withContext(apiDataRequest("POST", "/db/restore", slug, readFileSync(file, "utf8"), "application/sql"), restoreContext);
|
|
142
144
|
p.log.success(`Restored ${slug}.`);
|
|
143
145
|
})
|
|
146
|
+
}),
|
|
147
|
+
migrate: defineCommand({
|
|
148
|
+
meta: {
|
|
149
|
+
name: "migrate",
|
|
150
|
+
description: "Apply pending schema migrations to the project database"
|
|
151
|
+
},
|
|
152
|
+
args: {
|
|
153
|
+
...projectFlagArgs,
|
|
154
|
+
plan: {
|
|
155
|
+
type: "boolean",
|
|
156
|
+
default: false,
|
|
157
|
+
description: "Dry run: list pending migrations without applying anything"
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
run: reportErrors("db migrate", async ({ args }) => {
|
|
161
|
+
const { dir, slug } = resolveProject(args);
|
|
162
|
+
const files = readMigrationFiles(dir);
|
|
163
|
+
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).");
|
|
164
|
+
if (args.plan) {
|
|
165
|
+
const result = await withContext(migrateProject(slug, files, true), { action: `plan migrations for "${slug}"` });
|
|
166
|
+
if (result.pending.length === 0) console.log(`Nothing pending (${result.applied.length} applied previously).`);
|
|
167
|
+
else for (const name of result.pending) console.log(`pending ${name}`);
|
|
168
|
+
for (const line of describeMigrateResult(result)) console.log(line);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const spin = p.spinner();
|
|
172
|
+
spin.start(`Migrating ${slug}…`);
|
|
173
|
+
let result;
|
|
174
|
+
try {
|
|
175
|
+
result = await withContext(migrateProject(slug, files), { action: `migrate "${slug}"` });
|
|
176
|
+
} catch (err) {
|
|
177
|
+
spin.error("Migrate failed.");
|
|
178
|
+
throw err;
|
|
179
|
+
}
|
|
180
|
+
spin.stop(result.appliedNow.length === 0 ? `Nothing pending for ${slug}.` : `Applied ${result.appliedNow.length} migration(s) to ${slug}.`);
|
|
181
|
+
for (const line of describeMigrateResult(result)) console.log(` ${line}`);
|
|
182
|
+
})
|
|
144
183
|
})
|
|
145
184
|
}
|
|
146
185
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { c as apiRequest, i as unwrap, l as withContext, n as reportErrors, s as apiDownload } from "./ui-
|
|
1
|
+
import { a as projectDirArgs, c as resolveProject, l as tryReadCanopyJson, n as siteConfig, o as projectFlagArgs, s as projectUrl } from "./site-DQnysZ3W.mjs";
|
|
2
|
+
import { c as apiRequest, i as unwrap, l as withContext, n as reportErrors, o as apiDataRequest, s as apiDownload } from "./ui-BhzMeKQl.mjs";
|
|
3
|
+
import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-B0kJarjr.mjs";
|
|
3
4
|
import { defineCommand } from "citty";
|
|
4
5
|
import { mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
5
6
|
import { tmpdir } from "node:os";
|
|
@@ -46,6 +47,10 @@ async function bundleWorker(projectDir, entry) {
|
|
|
46
47
|
*
|
|
47
48
|
* Reads canopy.json in the project root:
|
|
48
49
|
* { "slug": "my-app", "dist": ".", "build": "npm run build" }
|
|
50
|
+
*
|
|
51
|
+
* A "migrations" dir in canopy.json adds a schema step (docs/13): pending
|
|
52
|
+
* migrations apply after the bundle is staged and before the version flips —
|
|
53
|
+
* schema first, then code. A migration failure aborts the deploy.
|
|
49
54
|
*/
|
|
50
55
|
/** Must match apps/api/src/routes/deploys.ts WORKER_BUNDLE_PATH. */
|
|
51
56
|
const WORKER_BUNDLE_PATH = "_canopy/worker.mjs";
|
|
@@ -97,12 +102,23 @@ const deployCommand = defineCommand({
|
|
|
97
102
|
default: true,
|
|
98
103
|
description: "Run the canopy.json build command first",
|
|
99
104
|
negativeDescription: "Skip the build, deploy the output directory as-is"
|
|
105
|
+
},
|
|
106
|
+
migrate: {
|
|
107
|
+
type: "boolean",
|
|
108
|
+
default: true,
|
|
109
|
+
description: "Apply pending schema migrations before the version goes live",
|
|
110
|
+
negativeDescription: "Skip migrations, deploy the code only"
|
|
100
111
|
}
|
|
101
112
|
},
|
|
102
113
|
run: reportErrors("deploy", async ({ args }) => {
|
|
103
114
|
const { dir, slug } = resolveProject(args);
|
|
104
115
|
const config = tryReadCanopyJson(dir) ?? {};
|
|
105
116
|
if (config.build && args.build) await runBuild(config.build, dir);
|
|
117
|
+
const migrations = args.migrate ? readMigrationFiles(dir) : void 0;
|
|
118
|
+
if (migrations !== void 0 && config.dialect) try {
|
|
119
|
+
const { dialect } = await apiDataRequest("GET", "/platform", slug);
|
|
120
|
+
if (dialect !== config.dialect) p.log.warn(`canopy.json pins dialect "${config.dialect}" but this site's database speaks ${dialect} — the migrations below were probably written for a different target.`);
|
|
121
|
+
} catch {}
|
|
106
122
|
const distDir = resolve(dir, config.dist ?? ".");
|
|
107
123
|
let paths;
|
|
108
124
|
try {
|
|
@@ -140,6 +156,7 @@ const deployCommand = defineCommand({
|
|
|
140
156
|
const workerNote = workerBytes ? " + worker" : "";
|
|
141
157
|
spin.start(`Deploying ${files.length} files${workerNote} (${(total / 1024).toFixed(0)} kB) → ${slug}`);
|
|
142
158
|
let result;
|
|
159
|
+
let migrateResult;
|
|
143
160
|
try {
|
|
144
161
|
const { upload } = await withContext(apiRequest("POST", `/projects/${slug}/deploys`, { worker: workerBytes !== null }, void 0, source), { action: `deploy "${slug}"` });
|
|
145
162
|
const uploads = [...files, ...workerBytes ? [{
|
|
@@ -147,6 +164,15 @@ const deployCommand = defineCommand({
|
|
|
147
164
|
data: workerBytes
|
|
148
165
|
}] : []];
|
|
149
166
|
await uploadFiles(upload.grant, uploads);
|
|
167
|
+
if (migrations !== void 0 && migrations.length > 0) {
|
|
168
|
+
spin.message(`Applying schema migrations → ${slug}`);
|
|
169
|
+
try {
|
|
170
|
+
migrateResult = await withContext(migrateProject(slug, migrations), { action: `migrate "${slug}"` });
|
|
171
|
+
} catch (err) {
|
|
172
|
+
spin.error("Deploy aborted — migrations failed; the live version is unchanged.");
|
|
173
|
+
throw err;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
150
176
|
({deploy: result} = await withContext(apiRequest("POST", `/projects/${slug}/deploys/${upload.version}/complete`, { grant: upload.grant }, void 0, source), { action: `deploy "${slug}"` }));
|
|
151
177
|
} catch (err) {
|
|
152
178
|
spin.error("Deploy failed.");
|
|
@@ -154,6 +180,7 @@ const deployCommand = defineCommand({
|
|
|
154
180
|
}
|
|
155
181
|
const backend = result.worker?.serviceUrl ? ` · backend ${result.worker.serviceUrl}` : "";
|
|
156
182
|
spin.stop(`Live: ${projectUrl(slug)} (v${result.version})${backend}`);
|
|
183
|
+
if (migrateResult !== void 0) for (const line of describeMigrateResult(migrateResult)) console.log(` ${line}`);
|
|
157
184
|
})
|
|
158
185
|
});
|
|
159
186
|
const UPLOAD_CONCURRENCY = 8;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as ApiError, c as apiRequest, l as withContext, n as reportErrors } from "./ui-
|
|
1
|
+
import { a as projectDirArgs, c as resolveProject, i as projectDialect, l as tryReadCanopyJson, n as siteConfig } from "./site-DQnysZ3W.mjs";
|
|
2
|
+
import { a as ApiError, c as apiRequest, l as withContext, n as reportErrors } from "./ui-BhzMeKQl.mjs";
|
|
3
|
+
import { r as readMigrationFiles } from "./migrations-B0kJarjr.mjs";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
4
5
|
import { defineCommand } from "citty";
|
|
5
6
|
import { join } from "node:path";
|
|
@@ -91,14 +92,19 @@ async function devRemote(flags, slug) {
|
|
|
91
92
|
} } });
|
|
92
93
|
console.log(`\n ${slug} — remote-first dev against LIVE project data`);
|
|
93
94
|
console.log(" (worried about a risky change? `canopy db snapshot` first, or work");
|
|
94
|
-
console.log(" on a local copy instead: `canopy dev --local --pull`)
|
|
95
|
+
console.log(" on a local copy instead: `canopy dev --local --pull`)");
|
|
96
|
+
if (tryReadCanopyJson(flags.dir)?.migrations) {
|
|
97
|
+
console.log(" migrations apply on `canopy deploy`, never from dev — use --local");
|
|
98
|
+
console.log(" while the schema is in flux");
|
|
99
|
+
}
|
|
100
|
+
console.log("");
|
|
95
101
|
server.printUrls();
|
|
96
102
|
console.log(`\n Dev token expires ${expires_at} — restart canopy dev after that.`);
|
|
97
103
|
}
|
|
98
104
|
async function devLocal(flags, slug) {
|
|
99
105
|
let dump;
|
|
100
106
|
if (flags.pull) {
|
|
101
|
-
const { fetchProjectDump } = await import("./local-
|
|
107
|
+
const { fetchProjectDump } = await import("./local-D9OVEHDg.mjs");
|
|
102
108
|
console.log(`Pulling a fresh export of ${slug}…`);
|
|
103
109
|
dump = await fetchProjectDump(slug);
|
|
104
110
|
}
|
|
@@ -113,16 +119,22 @@ async function devLocal(flags, slug) {
|
|
|
113
119
|
const reason = err instanceof ApiError ? err.explain().why : String(err);
|
|
114
120
|
console.log(`Note: ai/services will be unavailable — could not reach the platform (${reason}).`);
|
|
115
121
|
}
|
|
116
|
-
const { createSandbox } = await import("./local-
|
|
122
|
+
const { createSandbox } = await import("./local-D9OVEHDg.mjs");
|
|
117
123
|
const sandbox = await createSandbox({
|
|
118
124
|
slug,
|
|
119
125
|
persistDir: join(flags.dir, ".canopy", "local"),
|
|
120
|
-
email: siteConfig().email
|
|
126
|
+
email: siteConfig().email,
|
|
127
|
+
dialect: flags.dialect
|
|
121
128
|
});
|
|
122
129
|
if (dump !== void 0) {
|
|
123
130
|
await sandbox.applyDump(dump);
|
|
124
131
|
console.log(`Hydrated the sandbox (${(dump.length / 1024).toFixed(1)} kB of SQL).`);
|
|
125
132
|
}
|
|
133
|
+
const migrationFiles = readMigrationFiles(flags.dir);
|
|
134
|
+
if (migrationFiles !== void 0 && migrationFiles.length > 0) {
|
|
135
|
+
const { applied } = await sandbox.applyMigrations(migrationFiles);
|
|
136
|
+
if (applied.length > 0) console.log(`Applied ${applied.length} pending migration(s): ${applied.join(", ")}`);
|
|
137
|
+
}
|
|
126
138
|
const handleApi = async (req, res) => {
|
|
127
139
|
const chunks = [];
|
|
128
140
|
for await (const chunk of req) chunks.push(chunk);
|
|
@@ -170,8 +182,10 @@ async function devLocal(flags, slug) {
|
|
|
170
182
|
apiServer.close();
|
|
171
183
|
sandbox.dispose();
|
|
172
184
|
});
|
|
173
|
-
|
|
185
|
+
const engine = flags.dialect === "sqlite" ? "SQLite" : "PGlite";
|
|
186
|
+
console.log(`\n ${slug} — LOCAL sandbox (${engine} + filesystem blobs in .canopy/local/)`);
|
|
174
187
|
console.log(" data is yours alone: db and blob writes never reach the platform");
|
|
188
|
+
if (flags.dialect === void 0) console.log(" raw sql() speaks postgres here — on a D1 site? lock \"dialect\" in canopy.json");
|
|
175
189
|
if (dump === void 0) console.log(" (want prod data? restart with --pull to hydrate from a fresh export)");
|
|
176
190
|
console.log(" blobs are not pulled — the local blob store starts empty");
|
|
177
191
|
console.log(remote ? " ai/services pass through to the platform (metered as usual)" : " ai/services answer 501 until you log in and restart");
|
|
@@ -192,7 +206,7 @@ const devCommand = defineCommand({
|
|
|
192
206
|
local: {
|
|
193
207
|
type: "boolean",
|
|
194
208
|
default: false,
|
|
195
|
-
description: "Local-data sandbox (
|
|
209
|
+
description: "Local-data sandbox (database + blobs under .canopy/local/) instead of live data"
|
|
196
210
|
},
|
|
197
211
|
pull: {
|
|
198
212
|
type: "boolean",
|
|
@@ -209,7 +223,8 @@ const devCommand = defineCommand({
|
|
|
209
223
|
dir,
|
|
210
224
|
port,
|
|
211
225
|
local: args.local,
|
|
212
|
-
pull: args.pull
|
|
226
|
+
pull: args.pull,
|
|
227
|
+
dialect: projectDialect(dir)
|
|
213
228
|
};
|
|
214
229
|
await (flags.local ? devLocal(flags, slug) : devRemote(flags, slug));
|
|
215
230
|
})
|
package/dist/docs/ci.md
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: CI and deploy tokens
|
|
3
|
-
|
|
3
|
+
description: Push-to-main deploys using a project-scoped deploy token.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# CI and deploy tokens
|
|
7
|
-
|
|
8
6
|
Interactive `canopy login` is for humans at a terminal. For automated deploys
|
|
9
7
|
(GitHub Actions, etc.) use a **deploy token**: a project-scoped credential that
|
|
10
8
|
can deploy that one project and nothing else.
|
package/dist/docs/cli.md
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: CLI command reference
|
|
3
|
-
|
|
3
|
+
description: Every canopy command, its flags, and what it does.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# CLI command reference
|
|
7
|
-
|
|
8
6
|
Run `canopy <command> --help` for the authoritative flags of any command. This
|
|
9
|
-
is the overview.
|
|
7
|
+
is the one-page overview; the docs site's [CLI reference](https://docs.canopy.website/cli) section
|
|
8
|
+
has a full page per command.
|
|
10
9
|
|
|
11
10
|
## Auth
|
|
12
11
|
|
package/dist/docs/data.md
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Working with project data
|
|
3
|
-
|
|
3
|
+
description: Pulling, snapshotting, and restoring your project database.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Working with project data
|
|
7
|
-
|
|
8
6
|
Each Canopy project gets its own database and blob storage (D1 and R2 on the
|
|
9
7
|
Cloudflare target; Postgres and GCS on GCP). Your app reads and writes them
|
|
10
8
|
through the Canopy SDK at runtime; the CLI gives you tools to manage the data
|
|
@@ -65,7 +63,10 @@ risky change.
|
|
|
65
63
|
|
|
66
64
|
## Restore — destructive
|
|
67
65
|
|
|
68
|
-
Restoring **replaces ALL project data
|
|
66
|
+
> **Warning:** Restoring **replaces ALL project data**. Take a snapshot first — a snapshot taken seconds earlier
|
|
67
|
+
> is your undo button.
|
|
68
|
+
|
|
69
|
+
Restore from either source:
|
|
69
70
|
|
|
70
71
|
```bash
|
|
71
72
|
canopy db restore backup.sql # from a local dump
|
package/dist/docs/deploy.md
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Deploying a project
|
|
3
|
-
|
|
3
|
+
description: How canopy deploy builds, uploads, and makes a version live.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Deploying a project
|
|
7
|
-
|
|
8
6
|
From inside a project directory:
|
|
9
7
|
|
|
10
8
|
```bash
|
package/dist/docs/dev.md
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Local development
|
|
3
|
-
|
|
3
|
+
description: Running a project locally against live or sandboxed data.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Local development
|
|
7
|
-
|
|
8
6
|
From inside a project directory:
|
|
9
7
|
|
|
10
8
|
```bash
|
|
@@ -20,14 +18,14 @@ develop against realistic state.
|
|
|
20
18
|
## Offline sandbox
|
|
21
19
|
|
|
22
20
|
To develop without touching live data — for example on a plane, or before you
|
|
23
|
-
trust a destructive change — use a local sandbox backed by
|
|
24
|
-
|
|
21
|
+
trust a destructive change — use a local sandbox backed by an embedded database
|
|
22
|
+
and a filesystem blob store:
|
|
25
23
|
|
|
26
24
|
```bash
|
|
27
25
|
canopy dev --local
|
|
28
26
|
```
|
|
29
27
|
|
|
30
|
-
This creates local
|
|
28
|
+
This creates a local database and blobs under `.canopy/local/` in your
|
|
31
29
|
project. The sandbox starts empty.
|
|
32
30
|
|
|
33
31
|
To seed the sandbox from a fresh copy of production data first:
|
|
@@ -38,11 +36,15 @@ canopy dev --local --pull
|
|
|
38
36
|
|
|
39
37
|
`.canopy/local/` is disposable — delete it to reset the sandbox.
|
|
40
38
|
|
|
41
|
-
|
|
42
|
-
the postgres dialect there, while your deployed
|
|
43
|
-
platform target speaks (`canopy.db.dialect()`
|
|
44
|
-
Cloudflare). Collections (`canopy.db.collection(...)`)
|
|
45
|
-
so this only matters for hand-written SQL — see
|
|
39
|
+
By default the sandbox runs PGlite (embedded Postgres), so raw
|
|
40
|
+
`canopy.db.sql()` speaks the postgres dialect there, while your deployed
|
|
41
|
+
project speaks whatever its platform target speaks (`canopy.db.dialect()`
|
|
42
|
+
tells you — sqlite on Cloudflare). Collections (`canopy.db.collection(...)`)
|
|
43
|
+
behave the same in both, so this only matters for hand-written SQL — see
|
|
44
|
+
`canopy docs data`. If your project's raw SQL targets a sqlite site, lock the
|
|
45
|
+
dialect in `canopy.json` (`"dialect": "sqlite"`) and the sandbox runs an
|
|
46
|
+
embedded SQLite engine instead, so the same SQL works locally and deployed —
|
|
47
|
+
see [canopy dev](https://docs.canopy.website/cli/dev).
|
|
46
48
|
|
|
47
49
|
## Which mode should an agent use?
|
|
48
50
|
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Getting started
|
|
3
|
-
|
|
3
|
+
description: Install the CLI, sign in with OneLogin, and ship your first project.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Getting started with Canopy
|
|
7
|
-
|
|
8
6
|
Canopy is Monks' managed platform for vibecoded projects. You build a small web
|
|
9
7
|
app, and Canopy hosts it, gives it a database, blob storage, and AI — no
|
|
10
8
|
infrastructure to manage.
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
title: Infrastructure as code
|
|
3
|
+
description: Terraform for the account-level Cloudflare pieces Canopy needs.
|
|
4
|
+
---
|
|
2
5
|
|
|
3
6
|
Canopy's Cloudflare footprint is small on purpose: almost everything
|
|
4
7
|
per-project is provisioned at runtime by canopy-api (`apps/api/src/cf.ts`,
|
|
@@ -19,8 +22,8 @@ Scope of a fresh stand-up:
|
|
|
19
22
|
|
|
20
23
|
1. the **platform workers** — `canopy-api` and `canopy-gateway` — deployed and
|
|
21
24
|
healthy (these aren't optional; nothing works without them), and
|
|
22
|
-
2. the **
|
|
23
|
-
and deployed _on_ the platform like any other project.
|
|
25
|
+
2. the **four core projects** — `dashboard`, `www`, `docs`, `kitchen-sink` —
|
|
26
|
+
re-created and deployed _on_ the platform like any other project.
|
|
24
27
|
|
|
25
28
|
Everyone else's projects are **not** carried over — a new account starts with
|
|
26
29
|
an empty control-plane registry (see [Non-goals](#non-goals)).
|
|
@@ -39,8 +42,8 @@ config file per Cloudflare site (same names as CLI profiles). Deployed with
|
|
|
39
42
|
is local `wrangler dev` only.
|
|
40
43
|
|
|
41
44
|
**`canopy deploy`** (`canopy.json` in each project): the core projects
|
|
42
|
-
(`dashboard`, `www`, `kitchen-sink`) — ordinary platform projects
|
|
43
|
-
dogfood the product, not infrastructure.
|
|
45
|
+
(`dashboard`, `www`, `docs`, `kitchen-sink`) — ordinary platform projects
|
|
46
|
+
that dogfood the product, not infrastructure.
|
|
44
47
|
|
|
45
48
|
**Bootstrap scripts** (`infra/scripts/`): runtime API tokens, worker secrets,
|
|
46
49
|
AI Gateway BYOK keys + spend rule.
|
|
@@ -53,9 +56,9 @@ enable paid Workers products. Plus OneLogin: redirect URIs for the new host
|
|
|
53
56
|
## Non-goals
|
|
54
57
|
|
|
55
58
|
- **No project-data migration.** Per-project D1s, R2 blobs, and deploy history
|
|
56
|
-
stay in the old account. The new control-plane D1 is empty; the
|
|
57
|
-
projects are re-created from scratch (they hold no precious data — `www
|
|
58
|
-
`dashboard` are stateless, `kitchen-sink` is a demo), and everyone else's
|
|
59
|
+
stay in the old account. The new control-plane D1 is empty; the core
|
|
60
|
+
projects are re-created from scratch (they hold no precious data — `www`,
|
|
61
|
+
`dashboard`, and `docs` are stateless, `kitchen-sink` is a demo), and everyone else's
|
|
59
62
|
projects are re-created as needed. (If that ever changes, the export/import
|
|
60
63
|
primitives already exist in `apps/api/src/cf.ts` and `canopy db snapshot|restore`.)
|
|
61
64
|
- **No Tofu management of Workers/secrets/per-project resources** — see the
|
|
@@ -95,10 +98,15 @@ export CLOUDFLARE_ACCOUNT_ID="<account_id>" # required when you belong to >1 a
|
|
|
95
98
|
(You can confirm via API once the bootstrap token exists — an empty zone is
|
|
96
99
|
fine.)
|
|
97
100
|
4. **R2** must work (creating a probe bucket is enough to confirm). **Workers
|
|
98
|
-
Paid
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
101
|
+
Paid + Workers for Platforms** are required for per-project workers
|
|
102
|
+
(Phase 2 item 1, D18): once the subscriptions are active, run
|
|
103
|
+
`infra/scripts/create-dispatch-namespace.sh` to create the
|
|
104
|
+
`canopy-projects` dispatch namespace (the `dispatch_namespaces` bindings in
|
|
105
|
+
`apps/api/wrangler.main.jsonc` and `apps/gateway/wrangler.main.jsonc` fail
|
|
106
|
+
to deploy without it), and make sure `CF_API_TOKEN` carries **Workers
|
|
107
|
+
Scripts Write** (re-mint with `create-api-tokens.sh` if it predates this).
|
|
108
|
+
A site without the binding still runs — workers/schedules/secrets simply
|
|
109
|
+
answer 501. **No Advanced Certificate Manager** — `*.canopy.website`
|
|
102
110
|
is a single-level wildcard covered by free Universal SSL. (ACM would only
|
|
103
111
|
be needed if nesting as `*.canopy.<some-other-domain>`.)
|
|
104
112
|
|
|
@@ -283,8 +291,8 @@ If you already configured these for `canopy.website` before the move, skip.
|
|
|
283
291
|
|
|
284
292
|
### 10. Log in, grant yourself admin, then deploy the core projects
|
|
285
293
|
|
|
286
|
-
The
|
|
287
|
-
|
|
294
|
+
The four core projects are ordinary platform projects that dogfood the product.
|
|
295
|
+
Three of their slugs (`dashboard`, `www`, `docs`) are in `RESERVED_SLUGS`
|
|
288
296
|
(`apps/api/src/routes/projects.ts`), so **only an admin can create them** —
|
|
289
297
|
hence the ordering here.
|
|
290
298
|
|
|
@@ -326,6 +334,10 @@ export CANOPY_PROJECT_PROTOCOL="https"
|
|
|
326
334
|
canopy-dev --profile main create "Canopy" --slug www --no-scaffold
|
|
327
335
|
canopy-dev --profile main deploy
|
|
328
336
|
|
|
337
|
+
cd ../docs
|
|
338
|
+
canopy-dev --profile main create "Canopy Docs" --slug docs --no-scaffold
|
|
339
|
+
canopy-dev --profile main deploy
|
|
340
|
+
|
|
329
341
|
cd ../../examples/kitchen-sink
|
|
330
342
|
canopy-dev --profile main create "Kitchen Sink" --slug kitchen-sink --no-scaffold
|
|
331
343
|
# CF target: capabilities.workers = false — strip worker for this deploy
|
|
@@ -371,7 +383,7 @@ same as the old `thanarie.dev` setup). Client-password mode is the only
|
|
|
371
383
|
anonymous path. Smoke through a real browser session:
|
|
372
384
|
|
|
373
385
|
1. `https://dashboard.canopy.website` → OneLogin login works; the project list
|
|
374
|
-
shows all
|
|
386
|
+
shows all four under **Core projects**.
|
|
375
387
|
2. `https://www.canopy.website` → marketing site + `/changelog` render (after
|
|
376
388
|
SSO).
|
|
377
389
|
3. `https://kitchen-sink.canopy.website` → exercise `db`, `blob`, and `ai.chat`
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Migrations & Drizzle
|
|
3
|
+
description: Evolve your own tables with versioned SQL migrations — generated by Drizzle or written by hand — applied safely by canopy deploy.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
The document API (`canopy.db.collection(…)`) needs no schema and no migrations —
|
|
7
|
+
if it covers your project, skip this page. But developers who define their own
|
|
8
|
+
tables through raw `canopy.db.sql()` eventually need to _change_ those tables,
|
|
9
|
+
and that's what migrations are for: versioned `.sql` files in your repo,
|
|
10
|
+
applied exactly once each, in order, by the platform.
|
|
11
|
+
|
|
12
|
+
[Drizzle](https://orm.drizzle.team) is the recommended way to produce those
|
|
13
|
+
files (a typed schema plus generated SQL), but the mechanism is ORM-agnostic —
|
|
14
|
+
hand-written `.sql` files work identically.
|
|
15
|
+
|
|
16
|
+
## How it works
|
|
17
|
+
|
|
18
|
+
1. Your `canopy.json` names a migrations directory:
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"slug": "my-app",
|
|
23
|
+
"dialect": "sqlite",
|
|
24
|
+
"migrations": "./migrations"
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
2. The directory holds ordered `.sql` files (`0000_init.sql`, `0001_add_email.sql`, …),
|
|
29
|
+
applied in filename order.
|
|
30
|
+
|
|
31
|
+
3. The platform journals each applied file in the project database, so a file
|
|
32
|
+
applies **exactly once**, and takes an automatic **pre-migrate snapshot**
|
|
33
|
+
before applying anything — a bad migration is one
|
|
34
|
+
`canopy db restore` away.
|
|
35
|
+
|
|
36
|
+
4. Three surfaces apply them:
|
|
37
|
+
- **`canopy deploy`** — the main path. Pending migrations apply after the new
|
|
38
|
+
bundle is staged and before it goes live, so schema and code ship
|
|
39
|
+
together and the new code never sees the old schema. A failure aborts the
|
|
40
|
+
deploy with the old version still live.
|
|
41
|
+
- **`canopy dev --local`** — the sandbox applies pending migrations on boot,
|
|
42
|
+
so local dev always matches the repo.
|
|
43
|
+
- **`canopy db migrate [--plan]`** — apply (or dry-run) without deploying.
|
|
44
|
+
|
|
45
|
+
Remote-first `canopy dev` never applies migrations — it points at the
|
|
46
|
+
production database, and schema changes belong to deploys. While a schema
|
|
47
|
+
change is in flux, develop against `--local` (add `--pull` for a copy of real
|
|
48
|
+
data).
|
|
49
|
+
|
|
50
|
+
## The rules
|
|
51
|
+
|
|
52
|
+
- **Never edit an applied migration.** The platform hashes each file and
|
|
53
|
+
refuses edited history — write a new migration instead. (This is how every
|
|
54
|
+
mainstream migration tool works.)
|
|
55
|
+
- **Prefer additive changes** (new tables, new nullable columns), and do
|
|
56
|
+
removals in a _later_ release once nothing references the old shape. This
|
|
57
|
+
"expand/contract" habit makes every edge case boring.
|
|
58
|
+
- **Don't touch `_canopy_*` tables.** They're the platform's (the document
|
|
59
|
+
store, the migration journal) — the platform rejects migrations that
|
|
60
|
+
reference them.
|
|
61
|
+
- **There are no down-migrations.** Roll forward with a new migration, or
|
|
62
|
+
restore the automatic pre-migrate snapshot.
|
|
63
|
+
|
|
64
|
+
## Setting up Drizzle
|
|
65
|
+
|
|
66
|
+
Pin your dialect first — a Drizzle schema is written for one SQL flavor. Check
|
|
67
|
+
what your project's site runs on (`await canopy.db.dialect()` or the
|
|
68
|
+
[data guide](https://docs.canopy.website/guides/data)) and set `"dialect"` in `canopy.json`: `"sqlite"`
|
|
69
|
+
on Cloudflare-backed sites, `"postgres"` on GCP-backed ones. That same field
|
|
70
|
+
makes `canopy dev --local` run a matching local engine.
|
|
71
|
+
|
|
72
|
+
Install the tools and describe your schema:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm install drizzle-orm
|
|
76
|
+
npm install -D drizzle-kit
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```ts title="src/schema.ts"
|
|
80
|
+
import { sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
81
|
+
|
|
82
|
+
export const users = sqliteTable("users", {
|
|
83
|
+
id: text("id").primaryKey(),
|
|
84
|
+
email: text("email").notNull(),
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```ts title="drizzle.config.ts"
|
|
89
|
+
import { defineConfig } from "drizzle-kit";
|
|
90
|
+
|
|
91
|
+
export default defineConfig({
|
|
92
|
+
dialect: "sqlite", // match canopy.json "dialect" ("postgresql" for postgres)
|
|
93
|
+
schema: "./src/schema.ts",
|
|
94
|
+
out: "./migrations",
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
(On a postgres site, import from `drizzle-orm/pg-core` and set
|
|
99
|
+
`dialect: "postgresql"` — everything else is identical.)
|
|
100
|
+
|
|
101
|
+
Generate a migration whenever the schema changes:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
npx drizzle-kit generate
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
This is fully local — it diffs your schema files and writes `.sql` into
|
|
108
|
+
`./migrations`. **No database connection is involved, ever.** Then
|
|
109
|
+
`canopy deploy` (or `canopy db migrate`) applies it.
|
|
110
|
+
|
|
111
|
+
## Querying with Drizzle
|
|
112
|
+
|
|
113
|
+
Canopy never hands out connection strings — Drizzle talks to your data through
|
|
114
|
+
the handles Canopy already provides.
|
|
115
|
+
|
|
116
|
+
**In a project worker on a Cloudflare-backed site**, `env.DB` is a real D1
|
|
117
|
+
handle:
|
|
118
|
+
|
|
119
|
+
```ts title="worker/index.ts"
|
|
120
|
+
import { drizzle } from "drizzle-orm/d1";
|
|
121
|
+
import { users } from "../src/schema.ts";
|
|
122
|
+
|
|
123
|
+
export default {
|
|
124
|
+
async fetch(request, env) {
|
|
125
|
+
const db = drizzle(env.DB);
|
|
126
|
+
const all = await db.select().from(users);
|
|
127
|
+
return Response.json(all);
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**In the browser (or anywhere the SDK runs)**, wrap `canopy.db.sql()` with
|
|
133
|
+
Drizzle's proxy driver:
|
|
134
|
+
|
|
135
|
+
```ts title="src/db.ts"
|
|
136
|
+
import { drizzle } from "drizzle-orm/sqlite-proxy";
|
|
137
|
+
import { canopy } from "./canopy.ts";
|
|
138
|
+
|
|
139
|
+
export const db = drizzle(async (sql, params, method) => {
|
|
140
|
+
const rows = await canopy.db.sql<Record<string, unknown>>(sql, params);
|
|
141
|
+
const values = rows.map((row) => Object.values(row));
|
|
142
|
+
return { rows: method === "get" ? (values[0] ?? []) : values };
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
(On a postgres site use `drizzle-orm/pg-proxy` the same way; in a GCP project
|
|
147
|
+
worker, `env.DB.query(text, params)` fits `pg-proxy` directly.)
|
|
148
|
+
|
|
149
|
+
## What deliberately doesn't work
|
|
150
|
+
|
|
151
|
+
- **`drizzle-kit push` / `migrate` / `studio` / `introspect`** — these need a
|
|
152
|
+
database connection, which Canopy never exposes. Generating is local;
|
|
153
|
+
applying is Canopy's job. Don't invent a `DATABASE_URL` — there isn't one.
|
|
154
|
+
For browsing data, use the dashboard's data browser or `canopy db pull`.
|
|
155
|
+
- **Drizzle's runtime `migrate()`** — the platform journals migrations in its
|
|
156
|
+
own `_canopy_migrations` table; running Drizzle's applier would double-apply.
|
|
157
|
+
- **Cross-dialect portability.** A project with a Drizzle schema is pinned to
|
|
158
|
+
its dialect. The document API remains the portable path.
|
|
159
|
+
|
|
160
|
+
## Coexistence
|
|
161
|
+
|
|
162
|
+
Your Drizzle tables live alongside the document store in the same project
|
|
163
|
+
database. Snapshots, restore, `canopy db pull`, and the graduation export all
|
|
164
|
+
operate on the whole database, so they cover your tables automatically — and
|
|
165
|
+
since your schema history is in the repo and the journal travels with the
|
|
166
|
+
data, a graduated project takes its full migration story with it.
|