@mediamonks/canopy-cli 0.5.1 → 0.7.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-BkUmbGgx.mjs} +71 -15
- package/dist/{deploy-alC2--pZ.mjs → deploy-BcczXlSQ.mjs} +106 -8
- package/dist/dev-CvtoGFW0.mjs +2 -0
- package/dist/{dev-CQQhMOoD.mjs → dev-DvyKHe2d.mjs} +44 -14
- 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 +16 -4
- 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 +183 -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/staging.md +98 -0
- package/dist/docs/static-serving.md +4 -1
- package/dist/docs/troubleshooting.md +1 -3
- package/dist/{docs-DvSPpSIR.mjs → docs-2cUEug69.mjs} +23 -10
- package/dist/index.mjs +22 -20
- package/dist/{local-C5-GWJcx.mjs → local-C47tmhl7.mjs} +365 -41
- package/dist/{login-BO6abf40.mjs → login-ByD0hcxu.mjs} +5 -3
- package/dist/migrations-I1adj0z2.mjs +50 -0
- package/dist/{open-DnlYDqrU.mjs → open-DD9oa-Ho.mjs} +11 -4
- package/dist/{package-CHoZKzaw.mjs → package-CDB-uxy3.mjs} +1 -1
- package/dist/{projects-ijfe7Ffx.mjs → projects-CSH7KsPi.mjs} +17 -9
- package/dist/{site-BJjoePQS.mjs → site-BUlQR117.mjs} +14 -1
- package/dist/skill/canopy/SKILL.md +24 -1
- package/dist/{skill-CW0QSbAJ.mjs → skill-DrWzmUMv.mjs} +2 -2
- package/dist/staging-CfF2-pHs.mjs +113 -0
- package/dist/templates/spa/AGENTS.md +4 -0
- package/dist/templates/static/AGENTS.md +4 -0
- package/dist/templates/worker/AGENTS.md +7 -0
- package/dist/{token-BeF9zczg.mjs → token-nieJV1w7.mjs} +2 -2
- package/dist/{ui-DThlyHgo.mjs → ui-D8HxX0Ng.mjs} +3 -2
- package/dist/{upgrade-CGz1yzsW.mjs → upgrade-CH_jHkc7.mjs} +2 -2
- package/dist/{usage-DQpH7A3X.mjs → usage-qqPtkDtT.mjs} +2 -2
- package/package.json +2 -2
- 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-BUlQR117.mjs";
|
|
2
|
+
import { i as unwrap, l as withContext, n as reportErrors, o as apiDataRequest, r as requireTty } from "./ui-D8HxX0Ng.mjs";
|
|
3
|
+
import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-I1adj0z2.mjs";
|
|
3
4
|
import { defineCommand } from "citty";
|
|
4
5
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
5
6
|
import * as p from "@clack/prompts";
|
|
@@ -12,15 +13,24 @@ 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>.
|
|
18
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]" : "";
|
|
19
29
|
const formatSize = (bytes) => bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(1)} kB` : `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
20
30
|
const dbCommand = defineCommand({
|
|
21
31
|
meta: {
|
|
22
32
|
name: "db",
|
|
23
|
-
description: "Data tools: pull, snapshot, snapshots, restore"
|
|
33
|
+
description: "Data tools: pull, snapshot, snapshots, restore, migrate"
|
|
24
34
|
},
|
|
25
35
|
subCommands: {
|
|
26
36
|
pull: defineCommand({
|
|
@@ -30,6 +40,7 @@ const dbCommand = defineCommand({
|
|
|
30
40
|
},
|
|
31
41
|
args: {
|
|
32
42
|
...projectFlagArgs,
|
|
43
|
+
...stagingFlagArgs,
|
|
33
44
|
out: {
|
|
34
45
|
type: "string",
|
|
35
46
|
description: "Output file (defaults to <slug>-<timestamp>.sql)"
|
|
@@ -39,9 +50,9 @@ const dbCommand = defineCommand({
|
|
|
39
50
|
const { slug } = resolveProject(args);
|
|
40
51
|
const out = args.out ?? `${slug}-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replaceAll(/[:T]/g, "-")}.sql`;
|
|
41
52
|
const spin = p.spinner();
|
|
42
|
-
spin.start(`Exporting ${slug}…`);
|
|
53
|
+
spin.start(`Exporting ${slug}${channelNote(args)}…`);
|
|
43
54
|
try {
|
|
44
|
-
const { url } = await apiDataRequest("POST", "/db/export", slug);
|
|
55
|
+
const { url } = await apiDataRequest("POST", "/db/export", slug, void 0, void 0, channelOf(args));
|
|
45
56
|
const download = await fetch(url);
|
|
46
57
|
if (!download.ok) throw new Error(`Download failed (HTTP ${download.status}).`);
|
|
47
58
|
const sql = Buffer.from(await download.arrayBuffer());
|
|
@@ -58,14 +69,17 @@ const dbCommand = defineCommand({
|
|
|
58
69
|
name: "snapshot",
|
|
59
70
|
description: "Store a dump server-side (restorable anytime)"
|
|
60
71
|
},
|
|
61
|
-
args:
|
|
72
|
+
args: {
|
|
73
|
+
...projectFlagArgs,
|
|
74
|
+
...stagingFlagArgs
|
|
75
|
+
},
|
|
62
76
|
run: reportErrors("db snapshot", async ({ args }) => {
|
|
63
77
|
const { slug } = resolveProject(args);
|
|
64
78
|
const spin = p.spinner();
|
|
65
|
-
spin.start(`Snapshotting ${slug}…`);
|
|
79
|
+
spin.start(`Snapshotting ${slug}${channelNote(args)}…`);
|
|
66
80
|
let created;
|
|
67
81
|
try {
|
|
68
|
-
({snapshot: created} = await apiDataRequest("POST", "/db/snapshots", slug));
|
|
82
|
+
({snapshot: created} = await apiDataRequest("POST", "/db/snapshots", slug, void 0, void 0, channelOf(args)));
|
|
69
83
|
} catch (err) {
|
|
70
84
|
spin.error("Snapshot failed.");
|
|
71
85
|
throw err;
|
|
@@ -79,10 +93,13 @@ const dbCommand = defineCommand({
|
|
|
79
93
|
name: "snapshots",
|
|
80
94
|
description: "List stored snapshots"
|
|
81
95
|
},
|
|
82
|
-
args:
|
|
96
|
+
args: {
|
|
97
|
+
...projectFlagArgs,
|
|
98
|
+
...stagingFlagArgs
|
|
99
|
+
},
|
|
83
100
|
run: reportErrors("db snapshots", async ({ args }) => {
|
|
84
101
|
const { slug } = resolveProject(args);
|
|
85
|
-
const { snapshots } = await apiDataRequest("GET", "/db/snapshots", slug);
|
|
102
|
+
const { snapshots } = await apiDataRequest("GET", "/db/snapshots", slug, void 0, void 0, channelOf(args));
|
|
86
103
|
if (snapshots.length === 0) {
|
|
87
104
|
console.log(`No snapshots for ${slug} yet — run \`canopy db snapshot\`.`);
|
|
88
105
|
return;
|
|
@@ -97,6 +114,7 @@ const dbCommand = defineCommand({
|
|
|
97
114
|
},
|
|
98
115
|
args: {
|
|
99
116
|
...projectFlagArgs,
|
|
117
|
+
...stagingFlagArgs,
|
|
100
118
|
file: {
|
|
101
119
|
type: "positional",
|
|
102
120
|
description: "Local .sql dump to restore from",
|
|
@@ -118,7 +136,7 @@ const dbCommand = defineCommand({
|
|
|
118
136
|
let snapshotKey = args.snapshot;
|
|
119
137
|
if (!snapshotKey && !file) {
|
|
120
138
|
requireTty("pass a local .sql dump or --snapshot <key>.");
|
|
121
|
-
const { snapshots } = await apiDataRequest("GET", "/db/snapshots", slug);
|
|
139
|
+
const { snapshots } = await apiDataRequest("GET", "/db/snapshots", slug, void 0, void 0, channelOf(args));
|
|
122
140
|
if (snapshots.length === 0) throw new Error(`No snapshots for ${slug} — run \`canopy db snapshot\` first, or pass a local .sql dump.`);
|
|
123
141
|
snapshotKey = unwrap(await p.select({
|
|
124
142
|
message: `Snapshot to restore into ${slug}`,
|
|
@@ -131,15 +149,53 @@ const dbCommand = defineCommand({
|
|
|
131
149
|
}
|
|
132
150
|
if (!args.yes) {
|
|
133
151
|
requireTty("pass --yes to restore without confirmation.");
|
|
134
|
-
if (unwrap(await p.text({ message: `Restore will REPLACE all current data in "${slug}" with ${snapshotKey ?? file}. Type the slug to confirm.` })).trim() !== slug) {
|
|
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) {
|
|
135
153
|
p.cancel("Aborted.");
|
|
136
154
|
return;
|
|
137
155
|
}
|
|
138
156
|
}
|
|
139
157
|
const restoreContext = { action: `restore "${slug}" from ${snapshotKey ?? file}` };
|
|
140
|
-
if (snapshotKey) await withContext(apiDataRequest("POST", "/db/restore", slug, { snapshot: snapshotKey }), restoreContext);
|
|
141
|
-
else if (file) await withContext(apiDataRequest("POST", "/db/restore", slug, readFileSync(file, "utf8"), "application/sql"), restoreContext);
|
|
142
|
-
p.log.success(`Restored ${slug}.`);
|
|
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}`);
|
|
143
199
|
})
|
|
144
200
|
})
|
|
145
201
|
}
|
|
@@ -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 stagingUrl, n as siteConfig, o as projectFlagArgs, s as projectUrl, u as tryReadCanopyJson } from "./site-BUlQR117.mjs";
|
|
2
|
+
import { c as apiRequest, i as unwrap, l as withContext, n as reportErrors, o as apiDataRequest, s as apiDownload } from "./ui-D8HxX0Ng.mjs";
|
|
3
|
+
import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-I1adj0z2.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,29 @@ 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"
|
|
111
|
+
},
|
|
112
|
+
staging: {
|
|
113
|
+
type: "boolean",
|
|
114
|
+
default: false,
|
|
115
|
+
description: "Deploy to the project's staging channel (created on first use, database cloned from live)"
|
|
100
116
|
}
|
|
101
117
|
},
|
|
102
118
|
run: reportErrors("deploy", async ({ args }) => {
|
|
103
119
|
const { dir, slug } = resolveProject(args);
|
|
120
|
+
const channel = args.staging ? "staging" : "live";
|
|
104
121
|
const config = tryReadCanopyJson(dir) ?? {};
|
|
105
122
|
if (config.build && args.build) await runBuild(config.build, dir);
|
|
123
|
+
const migrations = args.migrate ? readMigrationFiles(dir) : void 0;
|
|
124
|
+
if (migrations !== void 0 && config.dialect) try {
|
|
125
|
+
const { dialect } = await apiDataRequest("GET", "/platform", slug);
|
|
126
|
+
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.`);
|
|
127
|
+
} catch {}
|
|
106
128
|
const distDir = resolve(dir, config.dist ?? ".");
|
|
107
129
|
let paths;
|
|
108
130
|
try {
|
|
@@ -138,22 +160,97 @@ const deployCommand = defineCommand({
|
|
|
138
160
|
const source = process.env["GITHUB_ACTIONS"] ? { "x-canopy-source": "github-action" } : {};
|
|
139
161
|
const spin = p.spinner();
|
|
140
162
|
const workerNote = workerBytes ? " + worker" : "";
|
|
141
|
-
|
|
163
|
+
const channelNote = channel === "staging" ? " [staging]" : "";
|
|
164
|
+
spin.start(`Deploying ${files.length} files${workerNote} (${(total / 1024).toFixed(0)} kB) → ${slug}${channelNote}`);
|
|
142
165
|
let result;
|
|
166
|
+
let migrateResult;
|
|
167
|
+
let stagingCreated = false;
|
|
143
168
|
try {
|
|
144
|
-
const { upload } = await withContext(apiRequest("POST", `/projects/${slug}/deploys`, {
|
|
169
|
+
const { upload } = await withContext(apiRequest("POST", `/projects/${slug}/deploys`, {
|
|
170
|
+
worker: workerBytes !== null,
|
|
171
|
+
channel
|
|
172
|
+
}, void 0, source), { action: `deploy "${slug}"` });
|
|
173
|
+
stagingCreated = upload.stagingCreated === true;
|
|
145
174
|
const uploads = [...files, ...workerBytes ? [{
|
|
146
175
|
path: WORKER_BUNDLE_PATH,
|
|
147
176
|
data: workerBytes
|
|
148
177
|
}] : []];
|
|
149
178
|
await uploadFiles(upload.grant, uploads);
|
|
179
|
+
if (migrations !== void 0 && migrations.length > 0) {
|
|
180
|
+
spin.message(`Applying schema migrations → ${slug}${channelNote}`);
|
|
181
|
+
try {
|
|
182
|
+
migrateResult = await withContext(migrateProject(slug, migrations, false, channel), { action: `migrate "${slug}"${channelNote}` });
|
|
183
|
+
} catch (err) {
|
|
184
|
+
spin.error(channel === "staging" ? "Deploy aborted — migrations failed on staging; live is unaffected." : "Deploy aborted — migrations failed; the live version is unchanged.");
|
|
185
|
+
throw err;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
150
188
|
({deploy: result} = await withContext(apiRequest("POST", `/projects/${slug}/deploys/${upload.version}/complete`, { grant: upload.grant }, void 0, source), { action: `deploy "${slug}"` }));
|
|
151
189
|
} catch (err) {
|
|
152
190
|
spin.error("Deploy failed.");
|
|
153
191
|
throw err;
|
|
154
192
|
}
|
|
155
193
|
const backend = result.worker?.serviceUrl ? ` · backend ${result.worker.serviceUrl}` : "";
|
|
156
|
-
|
|
194
|
+
if (channel === "staging") {
|
|
195
|
+
spin.stop(`Staging: ${stagingUrl(slug)} (v${result.version})${backend}`);
|
|
196
|
+
if (stagingCreated) console.log(" staging channel created — database cloned from live");
|
|
197
|
+
console.log(` happy with it? \`canopy promote\` makes v${result.version} live`);
|
|
198
|
+
} else spin.stop(`Live: ${projectUrl(slug)} (v${result.version})${backend}`);
|
|
199
|
+
if (migrateResult !== void 0) for (const line of describeMigrateResult(migrateResult)) console.log(` ${line}`);
|
|
200
|
+
})
|
|
201
|
+
});
|
|
202
|
+
const promoteCommand = defineCommand({
|
|
203
|
+
meta: {
|
|
204
|
+
name: "promote",
|
|
205
|
+
description: "Make the staged version live (migrations replay against live first)"
|
|
206
|
+
},
|
|
207
|
+
args: {
|
|
208
|
+
version: {
|
|
209
|
+
type: "positional",
|
|
210
|
+
description: "Version to promote, e.g. v3 (defaults to the staged version)",
|
|
211
|
+
required: false
|
|
212
|
+
},
|
|
213
|
+
...projectFlagArgs,
|
|
214
|
+
dir: {
|
|
215
|
+
type: "string",
|
|
216
|
+
description: "Project directory (defaults to the current directory)"
|
|
217
|
+
},
|
|
218
|
+
yes: {
|
|
219
|
+
type: "boolean",
|
|
220
|
+
default: false,
|
|
221
|
+
description: "Skip the confirmation prompt"
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
run: reportErrors("promote", async ({ args }) => {
|
|
225
|
+
const { slug } = resolveProject(args);
|
|
226
|
+
let target;
|
|
227
|
+
if (args.version !== void 0) {
|
|
228
|
+
if (!/^v?\d+$/.test(args.version)) throw new Error(`Invalid version "${args.version}" — expected a number like 3 or v3.`);
|
|
229
|
+
target = Number(args.version.replace(/^v/, ""));
|
|
230
|
+
} else {
|
|
231
|
+
const { staging } = await apiRequest("GET", `/projects/${slug}/staging`);
|
|
232
|
+
if (!staging.provisioned || staging.version === null) throw new Error(`Nothing staged for "${slug}" — run \`canopy deploy --staging\` first, or pass a version explicitly.`);
|
|
233
|
+
target = staging.version;
|
|
234
|
+
}
|
|
235
|
+
if (!args.yes && process.stdin.isTTY) {
|
|
236
|
+
if (!unwrap(await p.confirm({ message: `Promote v${target} of "${slug}" to live? Pending migrations replay against the live database first (snapshot taken automatically).` }))) {
|
|
237
|
+
p.cancel("Aborted.");
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const spin = p.spinner();
|
|
242
|
+
spin.start(`Promoting ${slug} v${target} → live`);
|
|
243
|
+
let result;
|
|
244
|
+
try {
|
|
245
|
+
({live: result} = await withContext(apiRequest("POST", `/projects/${slug}/deploys/${target}/promote`), { action: `promote "${slug}" v${target}` }));
|
|
246
|
+
} catch (err) {
|
|
247
|
+
spin.error("Promote failed — the live version is unchanged.");
|
|
248
|
+
throw err;
|
|
249
|
+
}
|
|
250
|
+
const backend = result.worker?.serviceUrl ? ` · backend ${result.worker.serviceUrl}` : "";
|
|
251
|
+
spin.stop(`Live: ${projectUrl(slug)} (v${result.version}, was v${result.previous ?? "?"})${backend}`);
|
|
252
|
+
for (const name of result.migrationsApplied) console.log(` applied ${name}`);
|
|
253
|
+
if (result.snapshot !== void 0) console.log(` pre-promote snapshot: ${result.snapshot} (canopy db restore --snapshot …)`);
|
|
157
254
|
})
|
|
158
255
|
});
|
|
159
256
|
const UPLOAD_CONCURRENCY = 8;
|
|
@@ -191,7 +288,7 @@ async function pickVersion(deploys, live, message, initial) {
|
|
|
191
288
|
const rollbackCommand = defineCommand({
|
|
192
289
|
meta: {
|
|
193
290
|
name: "rollback",
|
|
194
|
-
description: "Make an earlier deploy live again (pick from history)"
|
|
291
|
+
description: "Make an earlier deploy live again, backend included (pick from history)"
|
|
195
292
|
},
|
|
196
293
|
args: {
|
|
197
294
|
version: {
|
|
@@ -224,7 +321,8 @@ const rollbackCommand = defineCommand({
|
|
|
224
321
|
}
|
|
225
322
|
console.log(`Rolling back ${slug} to v${target}`);
|
|
226
323
|
const { live: result } = await apiRequest("POST", `/projects/${slug}/deploys/${target}/rollback`);
|
|
227
|
-
|
|
324
|
+
const worker = result.worker ? ` · worker v${result.worker.version}` : "";
|
|
325
|
+
console.log(`Live: ${projectUrl(slug)} (v${result.version}, was v${result.previous ?? "?"})${worker}`);
|
|
228
326
|
})
|
|
229
327
|
});
|
|
230
328
|
const downloadCommand = defineCommand({
|
|
@@ -275,4 +373,4 @@ const downloadCommand = defineCommand({
|
|
|
275
373
|
})
|
|
276
374
|
});
|
|
277
375
|
//#endregion
|
|
278
|
-
export { deployCommand, downloadCommand, rollbackCommand };
|
|
376
|
+
export { deployCommand, downloadCommand, promoteCommand, rollbackCommand };
|
|
@@ -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, n as siteConfig, u as tryReadCanopyJson } from "./site-BUlQR117.mjs";
|
|
2
|
+
import { a as ApiError, c as apiRequest, l as withContext, n as reportErrors } from "./ui-D8HxX0Ng.mjs";
|
|
3
|
+
import { r as readMigrationFiles } from "./migrations-I1adj0z2.mjs";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
4
5
|
import { defineCommand } from "citty";
|
|
5
6
|
import { join } from "node:path";
|
|
@@ -86,21 +87,34 @@ async function devRemote(flags, slug) {
|
|
|
86
87
|
const server = await startVite(flags, { proxy: { "/_api": {
|
|
87
88
|
target: apiUrl,
|
|
88
89
|
changeOrigin: true,
|
|
89
|
-
headers: {
|
|
90
|
+
headers: {
|
|
91
|
+
authorization: `Bearer ${token}`,
|
|
92
|
+
...flags.staging ? { "x-canopy-channel": "staging" } : {}
|
|
93
|
+
},
|
|
90
94
|
rewrite: rewriteApiPath
|
|
91
95
|
} } });
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
if (flags.staging) {
|
|
97
|
+
console.log(`\n ${slug} — remote-first dev against the STAGING channel's data`);
|
|
98
|
+
console.log(" (live data is untouched; `canopy staging sync` re-clones it from live)");
|
|
99
|
+
} else {
|
|
100
|
+
console.log(`\n ${slug} — remote-first dev against LIVE project data`);
|
|
101
|
+
console.log(" (worried about a risky change? `canopy db snapshot` first, or work");
|
|
102
|
+
console.log(" on a local copy instead: `canopy dev --local --pull`)");
|
|
103
|
+
}
|
|
104
|
+
if (tryReadCanopyJson(flags.dir)?.migrations && !flags.staging) {
|
|
105
|
+
console.log(" migrations apply on `canopy deploy`, never from dev — use --local");
|
|
106
|
+
console.log(" while the schema is in flux");
|
|
107
|
+
}
|
|
108
|
+
console.log("");
|
|
95
109
|
server.printUrls();
|
|
96
110
|
console.log(`\n Dev token expires ${expires_at} — restart canopy dev after that.`);
|
|
97
111
|
}
|
|
98
112
|
async function devLocal(flags, slug) {
|
|
99
113
|
let dump;
|
|
100
114
|
if (flags.pull) {
|
|
101
|
-
const { fetchProjectDump } = await import("./local-
|
|
102
|
-
console.log(`Pulling a fresh export of ${slug}…`);
|
|
103
|
-
dump = await fetchProjectDump(slug);
|
|
115
|
+
const { fetchProjectDump } = await import("./local-C47tmhl7.mjs");
|
|
116
|
+
console.log(`Pulling a fresh export of ${slug}${flags.staging ? " [staging]" : ""}…`);
|
|
117
|
+
dump = await fetchProjectDump(slug, flags.staging ? "staging" : "live");
|
|
104
118
|
}
|
|
105
119
|
let remote;
|
|
106
120
|
try {
|
|
@@ -113,16 +127,22 @@ async function devLocal(flags, slug) {
|
|
|
113
127
|
const reason = err instanceof ApiError ? err.explain().why : String(err);
|
|
114
128
|
console.log(`Note: ai/services will be unavailable — could not reach the platform (${reason}).`);
|
|
115
129
|
}
|
|
116
|
-
const { createSandbox } = await import("./local-
|
|
130
|
+
const { createSandbox } = await import("./local-C47tmhl7.mjs");
|
|
117
131
|
const sandbox = await createSandbox({
|
|
118
132
|
slug,
|
|
119
133
|
persistDir: join(flags.dir, ".canopy", "local"),
|
|
120
|
-
email: siteConfig().email
|
|
134
|
+
email: siteConfig().email,
|
|
135
|
+
dialect: flags.dialect
|
|
121
136
|
});
|
|
122
137
|
if (dump !== void 0) {
|
|
123
138
|
await sandbox.applyDump(dump);
|
|
124
139
|
console.log(`Hydrated the sandbox (${(dump.length / 1024).toFixed(1)} kB of SQL).`);
|
|
125
140
|
}
|
|
141
|
+
const migrationFiles = readMigrationFiles(flags.dir);
|
|
142
|
+
if (migrationFiles !== void 0 && migrationFiles.length > 0) {
|
|
143
|
+
const { applied } = await sandbox.applyMigrations(migrationFiles);
|
|
144
|
+
if (applied.length > 0) console.log(`Applied ${applied.length} pending migration(s): ${applied.join(", ")}`);
|
|
145
|
+
}
|
|
126
146
|
const handleApi = async (req, res) => {
|
|
127
147
|
const chunks = [];
|
|
128
148
|
for await (const chunk of req) chunks.push(chunk);
|
|
@@ -170,8 +190,10 @@ async function devLocal(flags, slug) {
|
|
|
170
190
|
apiServer.close();
|
|
171
191
|
sandbox.dispose();
|
|
172
192
|
});
|
|
173
|
-
|
|
193
|
+
const engine = flags.dialect === "sqlite" ? "SQLite" : "PGlite";
|
|
194
|
+
console.log(`\n ${slug} — LOCAL sandbox (${engine} + filesystem blobs in .canopy/local/)`);
|
|
174
195
|
console.log(" data is yours alone: db and blob writes never reach the platform");
|
|
196
|
+
if (flags.dialect === void 0) console.log(" raw sql() speaks postgres here — on a D1 site? lock \"dialect\" in canopy.json");
|
|
175
197
|
if (dump === void 0) console.log(" (want prod data? restart with --pull to hydrate from a fresh export)");
|
|
176
198
|
console.log(" blobs are not pulled — the local blob store starts empty");
|
|
177
199
|
console.log(remote ? " ai/services pass through to the platform (metered as usual)" : " ai/services answer 501 until you log in and restart");
|
|
@@ -192,24 +214,32 @@ const devCommand = defineCommand({
|
|
|
192
214
|
local: {
|
|
193
215
|
type: "boolean",
|
|
194
216
|
default: false,
|
|
195
|
-
description: "Local-data sandbox (
|
|
217
|
+
description: "Local-data sandbox (database + blobs under .canopy/local/) instead of live data"
|
|
196
218
|
},
|
|
197
219
|
pull: {
|
|
198
220
|
type: "boolean",
|
|
199
221
|
default: false,
|
|
200
222
|
description: "Hydrate the sandbox from a fresh prod export first (requires --local)"
|
|
223
|
+
},
|
|
224
|
+
staging: {
|
|
225
|
+
type: "boolean",
|
|
226
|
+
default: false,
|
|
227
|
+
description: "Point remote dev at the staging channel's data (or pull the staging dump with --local --pull)"
|
|
201
228
|
}
|
|
202
229
|
},
|
|
203
230
|
run: reportErrors("dev", async ({ args }) => {
|
|
204
231
|
const port = args.port === void 0 ? void 0 : Number(args.port);
|
|
205
232
|
if (port !== void 0 && !Number.isInteger(port)) throw new Error(`Invalid --port=${args.port}`);
|
|
206
233
|
if (args.pull && !args.local) throw new Error("--pull only makes sense with --local (remote dev already sees live data).");
|
|
234
|
+
if (args.staging && args.local && !args.pull) throw new Error("--staging with --local needs --pull (it selects which dump hydrates the sandbox).");
|
|
207
235
|
const { dir, slug } = resolveProject(args);
|
|
208
236
|
const flags = {
|
|
209
237
|
dir,
|
|
210
238
|
port,
|
|
211
239
|
local: args.local,
|
|
212
|
-
pull: args.pull
|
|
240
|
+
pull: args.pull,
|
|
241
|
+
staging: args.staging,
|
|
242
|
+
dialect: projectDialect(dir)
|
|
213
243
|
};
|
|
214
244
|
await (flags.local ? devLocal(flags, slug) : devRemote(flags, slug));
|
|
215
245
|
})
|
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
|
|
@@ -31,6 +29,19 @@ output directory (or the static files as-is for a `static` project):
|
|
|
31
29
|
canopy deploy --no-build
|
|
32
30
|
```
|
|
33
31
|
|
|
32
|
+
## Trying it on staging first
|
|
33
|
+
|
|
34
|
+
Nervous about a change — especially one with schema migrations or backend
|
|
35
|
+
code? Deploy it to the project's staging channel first, then promote the exact
|
|
36
|
+
same version once it checks out:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
canopy deploy --staging # own URL, own database (a copy of live)
|
|
40
|
+
canopy promote # make that version live
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
See [Staging & promoting](https://docs.canopy.website/guides/staging).
|
|
44
|
+
|
|
34
45
|
## Rolling back
|
|
35
46
|
|
|
36
47
|
Every deploy is kept, so you can instantly revert:
|
|
@@ -40,7 +51,8 @@ canopy rollback # pick a version from history
|
|
|
40
51
|
canopy rollback v3 # make v3 live again
|
|
41
52
|
```
|
|
42
53
|
|
|
43
|
-
Rollback re-points the live version at an existing deploy — it does not
|
|
54
|
+
Rollback re-points the live version at an existing deploy — it does not
|
|
55
|
+
rebuild, and the project's backend rolls back with it.
|
|
44
56
|
|
|
45
57
|
## Downloading a version
|
|
46
58
|
|
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.
|