@mediamonks/canopy-cli 0.7.8 → 0.7.10
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/{deploy-y75cgauK.mjs → deploy-BKFICJT1.mjs} +2 -1
- package/dist/dev-BYxSHUH6.mjs +2 -0
- package/dist/{dev-CxfnHPI4.mjs → dev-DtoZrexp.mjs} +61 -6
- package/dist/dev-worker-BZgtEM9V.mjs +313 -0
- package/dist/docs/cli-dev.md +19 -0
- package/dist/docs/cli-feedback.md +27 -11
- package/dist/docs/dev.md +19 -0
- package/dist/docs/overview-how-it-works.md +2 -1
- package/dist/docs/platform-client-access.md +2 -1
- package/dist/docs/platform-feedback.md +24 -11
- package/dist/docs/platform-members-and-access.md +2 -2
- package/dist/{docs-59NjixYd.mjs → docs-Ck9xogEV.mjs} +1 -1
- package/dist/{feedback-B7QOyMau.mjs → feedback-BpiCvItH.mjs} +9 -6
- package/dist/index.mjs +13 -13
- package/dist/{local-D-HcDDzL.mjs → local-C5AhA4GM.mjs} +1 -1
- package/dist/{login-Cxbj3F46.mjs → login-D7dI8D9N.mjs} +27 -11
- package/dist/{package-27IYBxoU.mjs → package-DRp7sI6i.mjs} +1 -1
- package/dist/{skill-7K_P-5oN.mjs → skill-B2sDMAmv.mjs} +1 -1
- package/dist/templates/worker/.dev.vars.example +9 -0
- package/dist/templates/worker/AGENTS.md +7 -1
- package/dist/templates/worker/README.md +4 -1
- package/dist/templates/worker/_gitignore +1 -0
- package/dist/{upgrade-CqADJbUX.mjs → upgrade-tOfSmAcc.mjs} +1 -1
- package/package.json +1 -1
- package/dist/dev-CQq2uzSz.mjs +0 -2
|
@@ -248,7 +248,8 @@ const promoteCommand = defineCommand({
|
|
|
248
248
|
throw err;
|
|
249
249
|
}
|
|
250
250
|
const backend = result.worker?.serviceUrl ? ` · backend ${result.worker.serviceUrl}` : "";
|
|
251
|
-
|
|
251
|
+
const was = result.previous === null ? "" : `, was v${result.previous}`;
|
|
252
|
+
spin.stop(`Live: ${projectUrl(slug)} (v${result.version}${was})${backend}`);
|
|
252
253
|
for (const name of result.migrationsApplied) console.log(` applied ${name}`);
|
|
253
254
|
if (result.snapshot !== void 0) console.log(` pre-promote snapshot: ${result.snapshot} (canopy db restore --snapshot …)`);
|
|
254
255
|
})
|
|
@@ -3,6 +3,7 @@ import { a as ApiError, c as apiRequest, n as reportErrors, u as withContext } f
|
|
|
3
3
|
import { r as readMigrationFiles } from "./migrations-C-5x6SYf.mjs";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { defineCommand } from "citty";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
6
7
|
import { join } from "node:path";
|
|
7
8
|
import { pathToFileURL } from "node:url";
|
|
8
9
|
//#region src/dev.ts
|
|
@@ -15,6 +16,56 @@ function rewriteApiPath(path) {
|
|
|
15
16
|
return path.slice(5);
|
|
16
17
|
}
|
|
17
18
|
/**
|
|
19
|
+
* What raw SQL through the worker's env.DB speaks (D15): the canopy.json lock
|
|
20
|
+
* wins, then the engine actually answering — PGlite in the --local sandbox
|
|
21
|
+
* (local.ts's default), the site's own database otherwise.
|
|
22
|
+
*/
|
|
23
|
+
function workerDialect(flags) {
|
|
24
|
+
if (flags.dialect !== void 0) return flags.dialect;
|
|
25
|
+
if (flags.local) return "postgres";
|
|
26
|
+
return siteConfig().cloud === "gcp" ? "postgres" : "sqlite";
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The project's own backend worker, served by the dev server the way the
|
|
30
|
+
* gateway serves it in production (dev-worker.ts). Returns the plugin plus
|
|
31
|
+
* the notice lines to print, or undefined when the project has no worker.
|
|
32
|
+
*/
|
|
33
|
+
async function workerPlugin(flags, slug) {
|
|
34
|
+
const entry = tryReadCanopyJson(flags.dir)?.worker;
|
|
35
|
+
if (entry === void 0) return void 0;
|
|
36
|
+
if (!existsSync(join(flags.dir, entry))) {
|
|
37
|
+
console.log(`note: canopy.json points "worker" at ${entry}, which does not exist — skipping.`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const { missingSecretNames, readDevVars, workerDevPlugin } = await import("./dev-worker-BZgtEM9V.mjs");
|
|
41
|
+
const secrets = readDevVars(flags.dir);
|
|
42
|
+
const email = siteConfig().email;
|
|
43
|
+
const plugin = workerDevPlugin({
|
|
44
|
+
dir: flags.dir,
|
|
45
|
+
entry,
|
|
46
|
+
slug,
|
|
47
|
+
channel: flags.staging ? "staging" : "live",
|
|
48
|
+
secrets,
|
|
49
|
+
user: email ? {
|
|
50
|
+
sub: "local-dev",
|
|
51
|
+
email,
|
|
52
|
+
name: "Local Dev"
|
|
53
|
+
} : null,
|
|
54
|
+
dialect: workerDialect(flags)
|
|
55
|
+
});
|
|
56
|
+
const notes = [` /api/* is served by ${entry} (edit and refresh; no build step)`];
|
|
57
|
+
if (Object.keys(secrets).length > 0) notes.push(` worker secrets from .dev.vars: ${Object.keys(secrets).sort().join(", ")}`);
|
|
58
|
+
const missing = missingSecretNames(((await apiRequest("GET", `/projects/${slug}/secrets`).catch(() => void 0))?.secrets ?? []).map((s) => s.key), Object.keys(secrets));
|
|
59
|
+
if (missing.length > 0) {
|
|
60
|
+
notes.push(` set in the dashboard but not in .dev.vars: ${missing.join(", ")}`);
|
|
61
|
+
notes.push(" (dashboard values are never downloaded — add local ones to .dev.vars)");
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
plugin,
|
|
65
|
+
notes
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
18
69
|
* Load Vite's `createServer`, preferring the Vite installed in the *project*
|
|
19
70
|
* over the copy bundled with the CLI. `bundled` reports which one we got.
|
|
20
71
|
*
|
|
@@ -43,7 +94,7 @@ async function loadCreateServer(dir) {
|
|
|
43
94
|
};
|
|
44
95
|
}
|
|
45
96
|
}
|
|
46
|
-
async function startVite(flags, serverOverrides) {
|
|
97
|
+
async function startVite(flags, serverOverrides, extraPlugins = []) {
|
|
47
98
|
const { createServer, bundled } = await loadCreateServer(flags.dir);
|
|
48
99
|
const plugins = [{
|
|
49
100
|
name: "canopy-platform-api-external",
|
|
@@ -61,7 +112,7 @@ async function startVite(flags, serverOverrides) {
|
|
|
61
112
|
});
|
|
62
113
|
const server = await createServer({
|
|
63
114
|
root: flags.dir,
|
|
64
|
-
plugins,
|
|
115
|
+
plugins: [...plugins, ...extraPlugins],
|
|
65
116
|
server: {
|
|
66
117
|
port: flags.port,
|
|
67
118
|
...serverOverrides
|
|
@@ -84,6 +135,7 @@ function devTokenContext(slug) {
|
|
|
84
135
|
async function devRemote(flags, slug) {
|
|
85
136
|
const { token, expires_at } = await withContext(apiRequest("POST", `/projects/${slug}/dev-token`), devTokenContext(slug));
|
|
86
137
|
const { apiUrl } = siteConfig();
|
|
138
|
+
const worker = await workerPlugin(flags, slug);
|
|
87
139
|
const server = await startVite(flags, { proxy: { "/_api": {
|
|
88
140
|
target: apiUrl,
|
|
89
141
|
changeOrigin: true,
|
|
@@ -92,7 +144,7 @@ async function devRemote(flags, slug) {
|
|
|
92
144
|
...flags.staging ? { "x-canopy-channel": "staging" } : {}
|
|
93
145
|
},
|
|
94
146
|
rewrite: rewriteApiPath
|
|
95
|
-
} } });
|
|
147
|
+
} } }, worker ? [worker.plugin] : []);
|
|
96
148
|
if (flags.staging) {
|
|
97
149
|
console.log(`\n ${slug} — remote-first dev against the STAGING channel's data`);
|
|
98
150
|
console.log(" (live data is untouched; `canopy staging sync` re-clones it from live)");
|
|
@@ -105,6 +157,7 @@ async function devRemote(flags, slug) {
|
|
|
105
157
|
console.log(" migrations apply on `canopy deploy`, never from dev — use --local");
|
|
106
158
|
console.log(" while the schema is in flux");
|
|
107
159
|
}
|
|
160
|
+
for (const note of worker?.notes ?? []) console.log(note);
|
|
108
161
|
console.log("");
|
|
109
162
|
server.printUrls();
|
|
110
163
|
console.log(`\n Dev token expires ${expires_at} — restart canopy dev after that.`);
|
|
@@ -112,7 +165,7 @@ async function devRemote(flags, slug) {
|
|
|
112
165
|
async function devLocal(flags, slug) {
|
|
113
166
|
let dump;
|
|
114
167
|
if (flags.pull) {
|
|
115
|
-
const { fetchProjectDump } = await import("./local-
|
|
168
|
+
const { fetchProjectDump } = await import("./local-C5AhA4GM.mjs");
|
|
116
169
|
console.log(`Pulling a fresh export of ${slug}${flags.staging ? " [staging]" : ""}…`);
|
|
117
170
|
dump = await fetchProjectDump(slug, flags.staging ? "staging" : "live");
|
|
118
171
|
}
|
|
@@ -127,7 +180,7 @@ async function devLocal(flags, slug) {
|
|
|
127
180
|
const reason = err instanceof ApiError ? err.explain().why : String(err);
|
|
128
181
|
console.log(`Note: ai/services will be unavailable — could not reach the platform (${reason}).`);
|
|
129
182
|
}
|
|
130
|
-
const { createSandbox } = await import("./local-
|
|
183
|
+
const { createSandbox } = await import("./local-C5AhA4GM.mjs");
|
|
131
184
|
const sandbox = await createSandbox({
|
|
132
185
|
slug,
|
|
133
186
|
persistDir: join(flags.dir, ".canopy", "local"),
|
|
@@ -176,6 +229,7 @@ async function devLocal(flags, slug) {
|
|
|
176
229
|
headers: { authorization: `Bearer ${remote.token}` },
|
|
177
230
|
rewrite: rewriteApiPath
|
|
178
231
|
};
|
|
232
|
+
const worker = await workerPlugin(flags, slug);
|
|
179
233
|
const server = await startVite(flags, { proxy: {
|
|
180
234
|
...remoteEntry && {
|
|
181
235
|
"/_api/data/v1/ai": remoteEntry,
|
|
@@ -185,7 +239,7 @@ async function devLocal(flags, slug) {
|
|
|
185
239
|
target: `http://127.0.0.1:${apiAddress.port}`,
|
|
186
240
|
rewrite: (path) => path
|
|
187
241
|
}
|
|
188
|
-
} });
|
|
242
|
+
} }, worker ? [worker.plugin] : []);
|
|
189
243
|
server.httpServer?.once("close", () => {
|
|
190
244
|
apiServer.close();
|
|
191
245
|
sandbox.dispose();
|
|
@@ -197,6 +251,7 @@ async function devLocal(flags, slug) {
|
|
|
197
251
|
if (dump === void 0) console.log(" (want prod data? restart with --pull to hydrate from a fresh export)");
|
|
198
252
|
console.log(" blobs are not pulled — the local blob store starts empty");
|
|
199
253
|
console.log(remote ? " ai/services pass through to the platform (metered as usual)" : " ai/services answer 501 until you log in and restart");
|
|
254
|
+
for (const note of worker?.notes ?? []) console.log(note);
|
|
200
255
|
console.log("");
|
|
201
256
|
server.printUrls();
|
|
202
257
|
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { relative, resolve, sep } from "node:path";
|
|
3
|
+
//#region src/dev-worker.ts
|
|
4
|
+
/**
|
|
5
|
+
* Paths that belong to the worker rather than the SPA — the gateway's rule
|
|
6
|
+
* (apps/gateway/src/backend.ts), kept identical so a route that works in dev
|
|
7
|
+
* is reachable in production.
|
|
8
|
+
*/
|
|
9
|
+
function isWorkerPath(pathname) {
|
|
10
|
+
return pathname === "/api" || pathname.startsWith("/api/");
|
|
11
|
+
}
|
|
12
|
+
/** The manual schedule tick, mirroring the shim's own path (docs/07). */
|
|
13
|
+
const SCHEDULED_PATH = "/__canopy/scheduled";
|
|
14
|
+
/**
|
|
15
|
+
* `.dev.vars` — local-only worker secrets, the wrangler-shaped file the repo
|
|
16
|
+
* already gitignores.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately NOT "pull the dashboard's secrets": there is no way to read a
|
|
19
|
+
* stored value back (apps/api/src/routes/secrets.ts lists names and
|
|
20
|
+
* timestamps only — values are AES-GCM at rest and injected into the worker
|
|
21
|
+
* runtime, nowhere else), and adding one would put every project's live
|
|
22
|
+
* third-party credentials on every laptop that runs `canopy dev`. North star
|
|
23
|
+
* principle 3, secure by default: a dev secret is a local secret.
|
|
24
|
+
*/
|
|
25
|
+
function parseDevVars(text) {
|
|
26
|
+
const out = {};
|
|
27
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
28
|
+
const line = raw.trim();
|
|
29
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
30
|
+
const withoutExport = line.startsWith("export ") ? line.slice(7).trim() : line;
|
|
31
|
+
const eq = withoutExport.indexOf("=");
|
|
32
|
+
if (eq <= 0) continue;
|
|
33
|
+
const key = withoutExport.slice(0, eq).trim();
|
|
34
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
|
|
35
|
+
let value = withoutExport.slice(eq + 1).trim();
|
|
36
|
+
if (value.startsWith("\"") && value.endsWith("\"") && value.length >= 2 || value.startsWith("'") && value.endsWith("'") && value.length >= 2) value = value.slice(1, -1);
|
|
37
|
+
out[key] = value;
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
/** Reads `.dev.vars` from the project dir; {} when there is none. */
|
|
42
|
+
function readDevVars(dir) {
|
|
43
|
+
try {
|
|
44
|
+
return parseDevVars(readFileSync(resolve(dir, ".dev.vars"), "utf8"));
|
|
45
|
+
} catch {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Dashboard secret names the local `.dev.vars` doesn't define. */
|
|
50
|
+
function missingSecretNames(remote, local) {
|
|
51
|
+
const have = new Set(local);
|
|
52
|
+
return remote.filter((name) => !have.has(name)).sort();
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Port of the shim's rewriteCanopyUrl: a worker may call
|
|
56
|
+
* `CANOPY.fetch("/db/todos")`, `"/data/v1/db/todos"` or the `/_api/…` form the
|
|
57
|
+
* browser SDK uses — all three land on the same data-plane path.
|
|
58
|
+
*/
|
|
59
|
+
function rewriteCanopyUrl(raw, base) {
|
|
60
|
+
const root = base.replace(/\/$/, "");
|
|
61
|
+
let path;
|
|
62
|
+
let search = "";
|
|
63
|
+
try {
|
|
64
|
+
const url = new URL(raw, "http://canopy.local");
|
|
65
|
+
path = url.pathname;
|
|
66
|
+
search = url.search;
|
|
67
|
+
} catch {
|
|
68
|
+
path = raw.startsWith("/") ? raw : `/${raw}`;
|
|
69
|
+
}
|
|
70
|
+
if (path.startsWith("/_api/")) path = path.slice(5);
|
|
71
|
+
if (!path.startsWith("/data/v1/") && !path.startsWith("/control/v1/")) path = `/data/v1${path.startsWith("/") ? path : `/${path}`}`;
|
|
72
|
+
return root + path + search;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* env.CANOPY.fetch — the same rewriting the shim does, delivered through the
|
|
76
|
+
* dev server's `/_api` mount so the running mode decides what answers.
|
|
77
|
+
* Credentials stay in the proxy; nothing here carries a token.
|
|
78
|
+
*/
|
|
79
|
+
function createCanopyFetch(apiBase, slug, channel, fetchImpl = fetch) {
|
|
80
|
+
return async (input, init) => {
|
|
81
|
+
const url = rewriteCanopyUrl(typeof input === "string" ? input : input instanceof URL ? input.href : input.url, apiBase());
|
|
82
|
+
const headers = new Headers(input instanceof Request ? input.headers : init?.headers);
|
|
83
|
+
headers.delete("host");
|
|
84
|
+
headers.delete("x-canopy-user-sub");
|
|
85
|
+
headers.delete("x-canopy-user-email");
|
|
86
|
+
headers.delete("x-canopy-user-name");
|
|
87
|
+
headers.delete("authorization");
|
|
88
|
+
headers.delete("cookie");
|
|
89
|
+
headers.set("x-canopy-project", slug);
|
|
90
|
+
if (channel === "staging") headers.set("x-canopy-channel", "staging");
|
|
91
|
+
const method = (input instanceof Request ? input.method : init?.method) ?? "GET";
|
|
92
|
+
const body = input instanceof Request ? input.body : init?.body;
|
|
93
|
+
const outbound = {
|
|
94
|
+
method,
|
|
95
|
+
headers,
|
|
96
|
+
body: body ?? void 0,
|
|
97
|
+
redirect: input instanceof Request ? input.redirect : init?.redirect
|
|
98
|
+
};
|
|
99
|
+
if (body) outbound.duplex = "half";
|
|
100
|
+
return fetchImpl(url, outbound);
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* env.DB over the data plane's raw-SQL escape hatch — the deployed worker
|
|
105
|
+
* holds a direct D1/Postgres handle, which a laptop has no credential for, so
|
|
106
|
+
* dev routes the same statements through `/db/sql` (the legacy uncapped shape
|
|
107
|
+
* the SDK uses). Reads are identical; writes report `rowCount: 0` because that
|
|
108
|
+
* shape returns rows, not a change count.
|
|
109
|
+
*/
|
|
110
|
+
function createDevDb(canopyFetch, dialect) {
|
|
111
|
+
return {
|
|
112
|
+
dialect,
|
|
113
|
+
query: async (text, params = []) => {
|
|
114
|
+
const response = await canopyFetch("/data/v1/db/sql", {
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers: { "content-type": "application/json" },
|
|
117
|
+
body: JSON.stringify({
|
|
118
|
+
query: text,
|
|
119
|
+
params
|
|
120
|
+
})
|
|
121
|
+
});
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
const detail = await response.text();
|
|
124
|
+
throw new Error(`env.DB query failed (HTTP ${response.status}): ${detail.slice(0, 300)}`);
|
|
125
|
+
}
|
|
126
|
+
const rows = await response.json();
|
|
127
|
+
return {
|
|
128
|
+
rows,
|
|
129
|
+
rowCount: rows.length
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** env.BLOB over the data plane's blob routes, for the same reason as env.DB. */
|
|
135
|
+
function createDevBlob(canopyFetch, prefix) {
|
|
136
|
+
const path = (key) => `/data/v1/blob/${encodeURIComponent(key)}`;
|
|
137
|
+
return {
|
|
138
|
+
prefix,
|
|
139
|
+
put: async (key, body, contentType) => {
|
|
140
|
+
const response = await canopyFetch(path(key), {
|
|
141
|
+
method: "PUT",
|
|
142
|
+
headers: { "content-type": contentType ?? "application/octet-stream" },
|
|
143
|
+
body
|
|
144
|
+
});
|
|
145
|
+
if (!response.ok) throw new Error(`env.BLOB put failed (HTTP ${response.status})`);
|
|
146
|
+
},
|
|
147
|
+
get: async (key) => {
|
|
148
|
+
const response = await canopyFetch(path(key));
|
|
149
|
+
if (response.status === 404) return null;
|
|
150
|
+
if (!response.ok) throw new Error(`env.BLOB get failed (HTTP ${response.status})`);
|
|
151
|
+
const contentType = response.headers.get("content-type") ?? void 0;
|
|
152
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
153
|
+
return contentType ? {
|
|
154
|
+
body: bytes,
|
|
155
|
+
contentType
|
|
156
|
+
} : { body: bytes };
|
|
157
|
+
},
|
|
158
|
+
list: async (sub) => {
|
|
159
|
+
const response = await canopyFetch(`/data/v1/blob${sub ? `?prefix=${encodeURIComponent(sub)}` : ""}`);
|
|
160
|
+
if (!response.ok) throw new Error(`env.BLOB list failed (HTTP ${response.status})`);
|
|
161
|
+
return (await response.json()).map(({ key, size }) => ({
|
|
162
|
+
key,
|
|
163
|
+
size
|
|
164
|
+
}));
|
|
165
|
+
},
|
|
166
|
+
delete: async (key) => {
|
|
167
|
+
const response = await canopyFetch(path(key), { method: "DELETE" });
|
|
168
|
+
if (!response.ok) throw new Error(`env.BLOB delete failed (HTTP ${response.status})`);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/** Gateway-stamped identity, honored in dev too so a request can pose as one. */
|
|
173
|
+
function userFromHeaders(headers, fallback) {
|
|
174
|
+
const sub = (headers.get("x-canopy-user-sub") ?? "").trim();
|
|
175
|
+
if (!sub) return fallback;
|
|
176
|
+
return {
|
|
177
|
+
sub,
|
|
178
|
+
email: (headers.get("x-canopy-user-email") ?? "").trim(),
|
|
179
|
+
name: (headers.get("x-canopy-user-name") ?? "").trim()
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* The env object the worker sees, in the shim's own key order: secrets first,
|
|
184
|
+
* platform keys after, so a stray `.dev.vars` entry can never shadow
|
|
185
|
+
* `CANOPY`/`DB`/`USER` (the dashboard rejects those names outright —
|
|
186
|
+
* apps/api/src/secrets.ts isReservedSecretKey).
|
|
187
|
+
*/
|
|
188
|
+
function buildWorkerEnv(options, request, parts) {
|
|
189
|
+
const blobPrefix = `projects/${options.slug}/`;
|
|
190
|
+
return {
|
|
191
|
+
...options.secrets,
|
|
192
|
+
CANOPY: { fetch: parts.canopyFetch },
|
|
193
|
+
DB: parts.db,
|
|
194
|
+
BLOB: parts.blob,
|
|
195
|
+
USER: userFromHeaders(request.headers, options.user),
|
|
196
|
+
PROJECT_SLUG: options.slug,
|
|
197
|
+
BLOB_PREFIX: blobPrefix,
|
|
198
|
+
CANOPY_CHANNEL: options.channel
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Host to reach the dev server on from inside this process. Vite's default
|
|
203
|
+
* bind is `localhost`, which on this machine is IPv6-only — a hardcoded
|
|
204
|
+
* 127.0.0.1 gets ECONNREFUSED — so the bound address decides.
|
|
205
|
+
*/
|
|
206
|
+
function loopbackHost(bound) {
|
|
207
|
+
if (bound === "" || bound === "::" || bound === "0.0.0.0") return "127.0.0.1";
|
|
208
|
+
return bound.includes(":") ? `[${bound}]` : bound;
|
|
209
|
+
}
|
|
210
|
+
/** Vite-root-relative URL for ssrLoadModule ("worker/index.ts" → "/worker/index.ts"). */
|
|
211
|
+
function entryModuleId(dir, entry) {
|
|
212
|
+
return `/${relative(resolve(dir), resolve(dir, entry)).split(sep).join("/")}`;
|
|
213
|
+
}
|
|
214
|
+
async function toRequest(req, origin) {
|
|
215
|
+
const headers = new Headers();
|
|
216
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
217
|
+
if (value === void 0) continue;
|
|
218
|
+
for (const one of Array.isArray(value) ? value : [value]) headers.append(name, one);
|
|
219
|
+
}
|
|
220
|
+
const method = req.method ?? "GET";
|
|
221
|
+
let body;
|
|
222
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
223
|
+
const chunks = [];
|
|
224
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
225
|
+
const buffer = Buffer.concat(chunks);
|
|
226
|
+
if (buffer.length > 0) body = new Uint8Array(buffer);
|
|
227
|
+
}
|
|
228
|
+
return new Request(`${origin}${req.url ?? "/"}`, {
|
|
229
|
+
method,
|
|
230
|
+
headers,
|
|
231
|
+
body
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
async function sendResponse(res, response) {
|
|
235
|
+
const headers = {};
|
|
236
|
+
for (const [name, value] of response.headers) headers[name] = value;
|
|
237
|
+
res.writeHead(response.status, headers);
|
|
238
|
+
res.end(Buffer.from(await response.arrayBuffer()));
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Serves the project's worker from the Vite dev server. Installed `pre` so
|
|
242
|
+
* `/api/*` is answered before Vite's SPA fallback claims it — the same
|
|
243
|
+
* precedence the gateway gives the worker over static assets.
|
|
244
|
+
*/
|
|
245
|
+
function workerDevPlugin(options) {
|
|
246
|
+
const moduleId = entryModuleId(options.dir, options.entry);
|
|
247
|
+
let apiBase = "";
|
|
248
|
+
const canopyFetch = createCanopyFetch(() => apiBase, options.slug, options.channel);
|
|
249
|
+
return {
|
|
250
|
+
name: "canopy-project-worker",
|
|
251
|
+
configureServer(server) {
|
|
252
|
+
const load = async () => {
|
|
253
|
+
const handler = (await server.ssrLoadModule(moduleId)).default;
|
|
254
|
+
if (!handler || typeof handler.fetch !== "function") throw new Error(`${options.entry} must default-export an object with fetch() (Workers shape).`);
|
|
255
|
+
return handler;
|
|
256
|
+
};
|
|
257
|
+
server.middlewares.use((req, res, next) => {
|
|
258
|
+
const path = (req.url ?? "/").split("?")[0] ?? "/";
|
|
259
|
+
const scheduled = path === SCHEDULED_PATH;
|
|
260
|
+
if (!isWorkerPath(path) && !scheduled) return next();
|
|
261
|
+
(async () => {
|
|
262
|
+
const address = server.httpServer?.address();
|
|
263
|
+
if (apiBase === "" && address != null && typeof address === "object") apiBase = `http://${loopbackHost(address.address)}:${address.port}/_api`;
|
|
264
|
+
const request = await toRequest(req, `http://${req.headers.host ?? "localhost"}`);
|
|
265
|
+
const worker = await load();
|
|
266
|
+
const env = buildWorkerEnv(options, request, {
|
|
267
|
+
canopyFetch,
|
|
268
|
+
db: createDevDb(canopyFetch, options.dialect),
|
|
269
|
+
blob: createDevBlob(canopyFetch, `projects/${options.slug}/`)
|
|
270
|
+
});
|
|
271
|
+
if (scheduled) {
|
|
272
|
+
if (req.method !== "POST") return next();
|
|
273
|
+
if (typeof worker.scheduled !== "function") {
|
|
274
|
+
await sendResponse(res, Response.json({
|
|
275
|
+
error: "no_scheduled_handler",
|
|
276
|
+
detail: "export scheduled() next to fetch"
|
|
277
|
+
}, { status: 501 }));
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const body = await request.json().catch(() => ({}));
|
|
281
|
+
const name = typeof body.name === "string" ? body.name : "";
|
|
282
|
+
if (!name) {
|
|
283
|
+
await sendResponse(res, Response.json({ error: "missing_name" }, { status: 400 }));
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
env.USER = null;
|
|
287
|
+
await worker.scheduled({
|
|
288
|
+
name,
|
|
289
|
+
cron: typeof body.cron === "string" ? body.cron : "",
|
|
290
|
+
scheduledTime: typeof body.scheduledTime === "number" ? body.scheduledTime : Date.now(),
|
|
291
|
+
type: "scheduled"
|
|
292
|
+
}, env);
|
|
293
|
+
await sendResponse(res, Response.json({
|
|
294
|
+
ok: true,
|
|
295
|
+
name
|
|
296
|
+
}));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
await sendResponse(res, await worker.fetch(request, env));
|
|
300
|
+
})().catch((err) => {
|
|
301
|
+
if (err instanceof Error) server.ssrFixStacktrace(err);
|
|
302
|
+
console.error("worker request failed", err);
|
|
303
|
+
sendResponse(res, Response.json({
|
|
304
|
+
error: "worker_error",
|
|
305
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
306
|
+
}, { status: 500 })).catch(() => res.end());
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
//#endregion
|
|
313
|
+
export { missingSecretNames, readDevVars, workerDevPlugin };
|
package/dist/docs/cli-dev.md
CHANGED
|
@@ -37,6 +37,25 @@ canopy dev --local --pull # sandbox hydrated from a fresh prod export
|
|
|
37
37
|
|
|
38
38
|
With the lock in place, `--local` runs on an embedded SQLite engine instead — the same raw SQL, full-text search behavior, and `--pull` dumps as your deployed site. `"dialect": "postgres"` makes today's default explicit. Each engine keeps its own state under `.canopy/local/`, so switching the lock never corrupts data. The SQLite engine uses Node's built-in database and needs **Node 24+** (older Node builds lack full-text search); the CLI says so plainly if yours is older.
|
|
39
39
|
|
|
40
|
+
**Your backend worker.** If `canopy.json` names a `worker` entry (the worker scaffold does), `canopy dev` serves it too: requests to `/api/*` go to your worker, everything else to the SPA — the same split the platform makes once you deploy. There's no build step; save the file and the next request runs the new code.
|
|
41
|
+
|
|
42
|
+
Your worker gets the same `env` it gets in production: `env.CANOPY.fetch` for the SDK, `env.DB` and `env.BLOB` for raw database and file access, `env.USER` for the signed-in person, `env.PROJECT_SLUG`. They all follow the mode you're in — live project data by default, your sandbox under `--local`. Two differences worth knowing: `env.USER` is you (the account you logged in with), since there's no company login in front of a local server, and a write through `env.DB` reports no row count locally.
|
|
43
|
+
|
|
44
|
+
To tick a schedule by hand, post its name to the same path the platform uses:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
curl -X POST localhost:5173/__canopy/scheduled -d '{"name":"nightly-digest"}'
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Worker secrets.** Secrets you set in the dashboard are never downloaded — the platform can't hand a stored value back, and your laptop is not where a live API key belongs. Put local values in a `.dev.vars` file next to `canopy.json` instead:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
STRIPE_KEY=sk_test_local
|
|
54
|
+
SLACK_WEBHOOK="https://hooks.slack.com/services/…"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Your worker reads them exactly as it reads dashboard secrets (`c.env.STRIPE_KEY`). Keep `.dev.vars` out of git — the scaffolds already ignore it. At startup `canopy dev` lists the dashboard secret names your `.dev.vars` doesn't define, so you know what's still missing (names only, never values).
|
|
58
|
+
|
|
40
59
|
**`--pull`.** Before the sandbox boots, the CLI requests a fresh SQL export of the project's production database and applies it locally. Sync is pull-only — nothing ever pushes back. Blobs are not pulled; the local blob store starts empty.
|
|
41
60
|
|
|
42
61
|
**Schema migrations.** When `canopy.json` configures a `"migrations"` directory (see the [migrations guide](https://docs.canopy.website/guides/migrations)), the `--local` sandbox applies pending migrations on boot, so the local schema always matches the repo. Remote-first dev **never** applies migrations — it points at production, and schema changes ship with `canopy deploy`. While a schema change is in flux, work in `--local`.
|
|
@@ -10,9 +10,11 @@ ones.
|
|
|
10
10
|
## Usage
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
canopy feedback list --status
|
|
13
|
+
canopy feedback list --status investigate
|
|
14
14
|
canopy feedback show <id>
|
|
15
15
|
canopy feedback comment <id> "root cause: the upload grant expires too early"
|
|
16
|
+
canopy feedback status <id> investigated --comment "root cause: the grant expires too early"
|
|
17
|
+
canopy feedback list --status accepted
|
|
16
18
|
canopy feedback status <id> resolved --comment "fixed in v42"
|
|
17
19
|
canopy feedback mine
|
|
18
20
|
canopy feedback report "canopy deploy exits 1 after upload" --command "canopy deploy"
|
|
@@ -28,8 +30,9 @@ require project membership. `list` and `show` take `--json` for
|
|
|
28
30
|
machine-readable output.
|
|
29
31
|
|
|
30
32
|
- `list [--status <s>] [--kind <k>]` — the project's inbox. Statuses:
|
|
31
|
-
`
|
|
32
|
-
`feedback`, `docs`.
|
|
33
|
+
`investigate`, `investigated`, `accepted`, `in_progress`, `resolved`,
|
|
34
|
+
`declined`; kinds: `bug`, `feedback`, `docs`. (Reports filed before triage
|
|
35
|
+
statuses existed sit in `new`, which you can still filter for.)
|
|
33
36
|
- `show <id> [--download <dir>]` — full detail: body, captured context (page
|
|
34
37
|
URL, deploy version, console errors — or CLI context), the screenshots on
|
|
35
38
|
the report, and the comment trail. `--download` saves the screenshots into
|
|
@@ -40,20 +43,33 @@ machine-readable output.
|
|
|
40
43
|
posted first and the screenshots hang off that trail entry — how you show
|
|
41
44
|
"here's what it looks like after the fix".
|
|
42
45
|
- `status <id> <new-status> [--comment <note>]` — move the ticket; the
|
|
43
|
-
transition is recorded on the trail.
|
|
46
|
+
transition is recorded on the trail. Moving to `investigated` needs the
|
|
47
|
+
findings: pass `--comment`, or comment first.
|
|
44
48
|
- `mine [--project <slug>] [--json]` — your own reports across all projects,
|
|
45
49
|
with current statuses.
|
|
46
50
|
- `report <text> [--kind bug|feedback] [--project <slug>] [--command <cmd>] [--error <output>] [--attach <file>] [--caption <text>]` —
|
|
47
51
|
file a new report. **Without `--project` it goes to the platform inbox**
|
|
48
52
|
(Canopy itself) — the right target when a `canopy` command misbehaves. The
|
|
49
53
|
CLI attaches its version, the active site, your platform, and the project
|
|
50
|
-
you were working in.
|
|
54
|
+
you were working in. Reports open as `investigate`; a project member may
|
|
55
|
+
pass `--status` to open one further along — `investigated` when the report
|
|
56
|
+
itself is the write-up of what you found, `accepted` when the person you are
|
|
57
|
+
working with asked for it.
|
|
51
58
|
|
|
52
59
|
## For agents
|
|
53
60
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
Two loops.
|
|
62
|
+
|
|
63
|
+
**Investigate:** `list --status investigate` is everything nobody has looked at
|
|
64
|
+
yet. Reproduce with the context in `show` (including the deploy version that
|
|
65
|
+
served the page), then `status <id> investigated --comment "<what you found>"`.
|
|
66
|
+
Stop there — say what it would take to fix and let the person decide.
|
|
67
|
+
|
|
68
|
+
**Build:** `list --status accepted` is what a person has asked for. Do the
|
|
69
|
+
work, then `resolve` with a note, or `declined` if it turns out to be a bad
|
|
70
|
+
idea. Set `accepted` yourself only when the person you are working with told
|
|
71
|
+
you to.
|
|
72
|
+
|
|
73
|
+
When a `canopy` command itself fails: rule out user error, check
|
|
74
|
+
`canopy feedback mine` for a duplicate, then `report` with the exact command
|
|
75
|
+
and error output.
|
package/dist/docs/dev.md
CHANGED
|
@@ -46,6 +46,25 @@ dialect in `canopy.json` (`"dialect": "sqlite"`) and the sandbox runs an
|
|
|
46
46
|
embedded SQLite engine instead, so the same SQL works locally and deployed —
|
|
47
47
|
see [canopy dev](https://docs.canopy.website/cli/dev).
|
|
48
48
|
|
|
49
|
+
## Backend worker
|
|
50
|
+
|
|
51
|
+
Projects with a `worker` entry in `canopy.json` get their backend locally too:
|
|
52
|
+
`/api/*` is served by your worker in both modes, with the same `env` it has in
|
|
53
|
+
production (`env.CANOPY`, `env.DB`, `env.BLOB`, `env.USER`). Edit and refresh —
|
|
54
|
+
there's no build step.
|
|
55
|
+
|
|
56
|
+
Secrets are the one thing that doesn't come from the dashboard. Stored values
|
|
57
|
+
are never downloadable, so put local ones in a `.dev.vars` file next to
|
|
58
|
+
`canopy.json`:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
STRIPE_KEY=sk_test_local
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Your worker reads them the same way it reads dashboard secrets
|
|
65
|
+
(`c.env.STRIPE_KEY`). `canopy dev` prints which dashboard secret names your
|
|
66
|
+
`.dev.vars` is missing. Keep the file out of git — the scaffolds ignore it.
|
|
67
|
+
|
|
49
68
|
## Which mode should an agent use?
|
|
50
69
|
|
|
51
70
|
- Iterating on UI or read-only features → `canopy dev` (live data, realistic).
|
|
@@ -29,7 +29,8 @@ address exists as soon as the project does.
|
|
|
29
29
|
Visitors to your project's URL hit OneLogin before they see anything. By
|
|
30
30
|
default any Monk can open it. From the project's Settings you can tighten that
|
|
31
31
|
to collaborators only, or add a shared client password so people outside Monks
|
|
32
|
-
can view it.
|
|
32
|
+
can view it. The password stays visible in Settings, so you can look it up and
|
|
33
|
+
share it again later. There is no "make it public" switch — that's by design.
|
|
33
34
|
|
|
34
35
|
## Deploys create versions
|
|
35
36
|
|
|
@@ -28,7 +28,8 @@ The unlock is deliberately narrow:
|
|
|
28
28
|
|
|
29
29
|
## Enabling it
|
|
30
30
|
|
|
31
|
-
Open the project's **Settings** tab and use the **
|
|
31
|
+
Open the project's **Settings** tab and use the **Who outside Monks can open
|
|
32
|
+
it** card:
|
|
32
33
|
|
|
33
34
|
1. Set a password. Pick something you're comfortable saying out loud on a
|
|
34
35
|
call — it's a shared secret, not a personal credential.
|
|
@@ -55,8 +55,15 @@ report itself, and are deleted with it.
|
|
|
55
55
|
|
|
56
56
|
Project members triage reports on the **Feedback tab**:
|
|
57
57
|
|
|
58
|
-
- Statuses: `
|
|
59
|
-
|
|
58
|
+
- Statuses: `investigate → investigated → accepted → in_progress → resolved`,
|
|
59
|
+
plus `declined`.
|
|
60
|
+
- Everything arrives as **investigate** — nobody has looked at it yet. An agent
|
|
61
|
+
can pick those up, work out what is actually going on, and write the findings
|
|
62
|
+
on the report as **investigated**.
|
|
63
|
+
- **`accepted` means a person said "build this."** It is the one status an
|
|
64
|
+
agent should never reach for on its own.
|
|
65
|
+
- The order is a suggestion, not a track. Accept something straight away, skip
|
|
66
|
+
the investigation, or decline it at any point.
|
|
60
67
|
- Every status change and comment is recorded on the item's trail.
|
|
61
68
|
- Reporters follow their submissions on the **Reports page** (dashboard
|
|
62
69
|
header) and can comment on or withdraw their own items; only project
|
|
@@ -64,21 +71,27 @@ Project members triage reports on the **Feedback tab**:
|
|
|
64
71
|
|
|
65
72
|
## Working the backlog with an agent
|
|
66
73
|
|
|
67
|
-
Because the CLI exposes the inbox, "
|
|
68
|
-
|
|
74
|
+
Because the CLI exposes the inbox, "look into the new reports" and "work
|
|
75
|
+
through the accepted feedback" are both prompts, not processes:
|
|
69
76
|
|
|
70
77
|
```bash
|
|
71
|
-
canopy feedback list --status
|
|
72
|
-
canopy feedback show <id>
|
|
73
|
-
canopy feedback
|
|
78
|
+
canopy feedback list --status investigate # what nobody has looked at yet
|
|
79
|
+
canopy feedback show <id> # body + captured context + trail
|
|
80
|
+
canopy feedback status <id> investigated \
|
|
81
|
+
--comment "root cause: the upload grant expires too early"
|
|
82
|
+
canopy feedback list --status accepted # what a person asked for
|
|
74
83
|
canopy feedback status <id> resolved --comment "fixed in v42"
|
|
75
84
|
```
|
|
76
85
|
|
|
86
|
+
`investigated` always carries the findings — the note is required, because a
|
|
87
|
+
report that claims someone looked into it and shows nothing is worse than an
|
|
88
|
+
untouched one. Then it waits: **accepting is the person's call**, so an agent
|
|
89
|
+
sets `accepted` only when the person it is working with says to build the
|
|
90
|
+
thing.
|
|
91
|
+
|
|
77
92
|
`show` includes the deploy version that served the page when the bug was
|
|
78
|
-
reported — reproduce against that, not whatever is live now.
|
|
79
|
-
|
|
80
|
-
for a human; one that pushes back should `status <id> declined` with a comment
|
|
81
|
-
saying why.
|
|
93
|
+
reported — reproduce against that, not whatever is live now. A report worth
|
|
94
|
+
pushing back on gets `status <id> declined` with a comment saying why.
|
|
82
95
|
|
|
83
96
|
## Reporting problems with Canopy itself
|
|
84
97
|
|
|
@@ -44,8 +44,8 @@ from the same card.
|
|
|
44
44
|
|
|
45
45
|
Sometimes "any Monk can look" is too open — payroll prototypes, sensitive
|
|
46
46
|
client work, unfinished things you'd rather not demo by accident. Flip on
|
|
47
|
-
**Settings →
|
|
48
|
-
project's roster:
|
|
47
|
+
**Settings → Who at Monks can open it → Restrict to collaborators** and the
|
|
48
|
+
live app is limited to the project's roster:
|
|
49
49
|
|
|
50
50
|
- Other Monks who open the URL get a clear "limited to its collaborators"
|
|
51
51
|
page instead of the app.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-DRp7sI6i.mjs";
|
|
2
2
|
import { n as siteConfig } from "./site-BUlQR117.mjs";
|
|
3
3
|
import { i as latestKnownVersion, r as compareSemver } from "./version-check-CcsA1QjI.mjs";
|
|
4
4
|
import { n as reportErrors } from "./ui-2mrJcjYC.mjs";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-DRp7sI6i.mjs";
|
|
2
2
|
import { a as projectDirArgs, c as resolveProject, n as siteConfig, u as tryReadCanopyJson } from "./site-BUlQR117.mjs";
|
|
3
3
|
import { c as apiRequest, l as apiUpload, n as reportErrors, s as apiDownload } from "./ui-2mrJcjYC.mjs";
|
|
4
4
|
import { defineCommand } from "citty";
|
|
@@ -9,19 +9,22 @@ import { readFile, writeFile } from "node:fs/promises";
|
|
|
9
9
|
* canopy feedback — the ticket-lite feedback system (docs/19, docs topic
|
|
10
10
|
* `platform-feedback`). Two jobs:
|
|
11
11
|
*
|
|
12
|
-
* - Work a project's inbox: `list`, `show`, `comment`, `status` —
|
|
13
|
-
*
|
|
12
|
+
* - Work a project's inbox: `list`, `show`, `comment`, `status` — investigate
|
|
13
|
+
* what came in (`investigate` → comment the findings → `investigated`), and
|
|
14
|
+
* build what a human marked `accepted`.
|
|
14
15
|
* - File reports: `report` sends a bug/feedback item. With no `--project` it
|
|
15
16
|
* lands on the **platform inbox** (canopy itself) — the right target when a
|
|
16
17
|
* canopy command misbehaves while you're building something else.
|
|
17
18
|
*/
|
|
18
19
|
const STATUSES = [
|
|
19
|
-
"
|
|
20
|
+
"investigate",
|
|
21
|
+
"investigated",
|
|
20
22
|
"accepted",
|
|
21
23
|
"in_progress",
|
|
22
24
|
"resolved",
|
|
23
25
|
"declined"
|
|
24
26
|
];
|
|
27
|
+
const FILTER_STATUSES = [...STATUSES, "new"];
|
|
25
28
|
const KINDS = [
|
|
26
29
|
"bug",
|
|
27
30
|
"feedback",
|
|
@@ -94,7 +97,7 @@ const listCommand = defineCommand({
|
|
|
94
97
|
...jsonArg,
|
|
95
98
|
status: {
|
|
96
99
|
type: "string",
|
|
97
|
-
description: `Filter: ${
|
|
100
|
+
description: `Filter: ${FILTER_STATUSES.join(" | ")}`
|
|
98
101
|
},
|
|
99
102
|
kind: {
|
|
100
103
|
type: "string",
|
|
@@ -219,7 +222,7 @@ const statusCommand = defineCommand({
|
|
|
219
222
|
},
|
|
220
223
|
comment: {
|
|
221
224
|
type: "string",
|
|
222
|
-
description: "
|
|
225
|
+
description: "Note recorded with the change (required for `investigated`)"
|
|
223
226
|
},
|
|
224
227
|
...projectDirArgs
|
|
225
228
|
},
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as version } from "./package-
|
|
2
|
+
import { t as version } from "./package-DRp7sI6i.mjs";
|
|
3
3
|
import { r as siteHeaderLine, t as setInvocationProfile } from "./site-BUlQR117.mjs";
|
|
4
4
|
import { a as notifyOfUpdate, o as runRefresh } from "./version-check-CcsA1QjI.mjs";
|
|
5
5
|
import { defineCommand, runMain } from "citty";
|
|
@@ -60,28 +60,28 @@ const main = defineCommand({
|
|
|
60
60
|
description: "canopy — create and deploy vibecoded projects"
|
|
61
61
|
},
|
|
62
62
|
subCommands: {
|
|
63
|
-
login: () => import("./login-
|
|
64
|
-
whoami: () => import("./login-
|
|
65
|
-
logout: () => import("./login-
|
|
63
|
+
login: () => import("./login-D7dI8D9N.mjs").then((m) => m.loginCommand),
|
|
64
|
+
whoami: () => import("./login-D7dI8D9N.mjs").then((m) => m.whoamiCommand),
|
|
65
|
+
logout: () => import("./login-D7dI8D9N.mjs").then((m) => m.logoutCommand),
|
|
66
66
|
create: () => import("./projects-C6JQjVqg.mjs").then((m) => m.createCommand),
|
|
67
67
|
init: () => import("./projects-C6JQjVqg.mjs").then((m) => m.initCommand),
|
|
68
68
|
projects: () => import("./projects-C6JQjVqg.mjs").then((m) => m.projectsCommand),
|
|
69
69
|
delete: () => import("./projects-C6JQjVqg.mjs").then((m) => m.deleteCommand),
|
|
70
|
-
dev: () => import("./dev-
|
|
71
|
-
deploy: () => import("./deploy-
|
|
72
|
-
promote: () => import("./deploy-
|
|
70
|
+
dev: () => import("./dev-BYxSHUH6.mjs").then((m) => m.devCommand),
|
|
71
|
+
deploy: () => import("./deploy-BKFICJT1.mjs").then((m) => m.deployCommand),
|
|
72
|
+
promote: () => import("./deploy-BKFICJT1.mjs").then((m) => m.promoteCommand),
|
|
73
73
|
staging: () => import("./staging-Bw1D_J8M.mjs").then((m) => m.stagingCommand),
|
|
74
|
-
rollback: () => import("./deploy-
|
|
75
|
-
download: () => import("./deploy-
|
|
74
|
+
rollback: () => import("./deploy-BKFICJT1.mjs").then((m) => m.rollbackCommand),
|
|
75
|
+
download: () => import("./deploy-BKFICJT1.mjs").then((m) => m.downloadCommand),
|
|
76
76
|
token: () => import("./token-D7zrIF8G.mjs").then((m) => m.tokenCommand),
|
|
77
77
|
egress: () => import("./egress-CN_axsAN.mjs").then((m) => m.egressCommand),
|
|
78
|
-
feedback: () => import("./feedback-
|
|
78
|
+
feedback: () => import("./feedback-BpiCvItH.mjs").then((m) => m.feedbackCommand),
|
|
79
79
|
db: () => import("./db-8Etjmzxh.mjs").then((m) => m.dbCommand),
|
|
80
80
|
usage: () => import("./usage-CX1Izj_l.mjs").then((m) => m.usageCommand),
|
|
81
81
|
open: () => import("./open-DRPp4QJg.mjs").then((m) => m.openCommand),
|
|
82
|
-
docs: () => import("./docs-
|
|
83
|
-
skill: () => import("./skill-
|
|
84
|
-
upgrade: () => import("./upgrade-
|
|
82
|
+
docs: () => import("./docs-Ck9xogEV.mjs").then((m) => m.docsCommand),
|
|
83
|
+
skill: () => import("./skill-B2sDMAmv.mjs").then((m) => m.skillCommand),
|
|
84
|
+
upgrade: () => import("./upgrade-tOfSmAcc.mjs").then((m) => m.upgradeCommand),
|
|
85
85
|
logs: () => import("./logs-DNiZquki.mjs").then((m) => m.logsCommand)
|
|
86
86
|
}
|
|
87
87
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { o as apiDataRequest } from "./ui-2mrJcjYC.mjs";
|
|
2
|
-
import { n as rewriteApiPath } from "./dev-
|
|
2
|
+
import { n as rewriteApiPath } from "./dev-DtoZrexp.mjs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { dirname, join, relative, sep } from "node:path";
|
|
@@ -19,27 +19,43 @@ function loginCallbackPage(ok) {
|
|
|
19
19
|
<title>${title} · Canopy</title>
|
|
20
20
|
<style>
|
|
21
21
|
:root {
|
|
22
|
-
color-scheme: light;
|
|
22
|
+
color-scheme: light dark;
|
|
23
23
|
--ink: #14201a;
|
|
24
24
|
--muted: #4a5c52;
|
|
25
25
|
--line: #d5ddd7;
|
|
26
26
|
--panel: rgba(255, 255, 255, 0.82);
|
|
27
|
+
--field: rgba(255, 255, 255, 0.7);
|
|
27
28
|
--accent: #2f6b4f;
|
|
28
|
-
--
|
|
29
|
-
--
|
|
30
|
-
--
|
|
29
|
+
--mark-ink: #f4faf6;
|
|
30
|
+
--shadow: 0 18px 40px -28px rgba(20, 32, 26, 0.45);
|
|
31
|
+
--bg: radial-gradient(900px 480px at 12% -10%, #dfece3, transparent 60%),
|
|
32
|
+
radial-gradient(700px 420px at 100% 0%, #e7efe4, transparent 55%),
|
|
33
|
+
linear-gradient(165deg, #e8f0ea, #f7f5f0 55%, #eef2ef);
|
|
31
34
|
--mark-bg: ${ok ? "var(--accent)" : "#8a4b3a"};
|
|
32
35
|
}
|
|
36
|
+
@media (prefers-color-scheme: dark) {
|
|
37
|
+
:root {
|
|
38
|
+
--ink: #e9f0ec;
|
|
39
|
+
--muted: #a2b3aa;
|
|
40
|
+
--line: rgba(233, 240, 236, 0.14);
|
|
41
|
+
--panel: rgba(24, 31, 27, 0.72);
|
|
42
|
+
--field: rgba(233, 240, 236, 0.06);
|
|
43
|
+
--accent: #7cc39d;
|
|
44
|
+
--mark-ink: #0d1410;
|
|
45
|
+
--shadow: 0 18px 44px -26px rgba(0, 0, 0, 0.7);
|
|
46
|
+
--bg: radial-gradient(900px 480px at 12% -10%, #16261d, transparent 60%),
|
|
47
|
+
radial-gradient(700px 420px at 100% 0%, #16221c, transparent 55%),
|
|
48
|
+
linear-gradient(165deg, #0e1512, #121a15 55%, #0d130f);
|
|
49
|
+
--mark-bg: ${ok ? "var(--accent)" : "#e0a292"};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
33
52
|
* { box-sizing: border-box; }
|
|
34
53
|
body {
|
|
35
54
|
margin: 0;
|
|
36
55
|
min-height: 100vh;
|
|
37
56
|
font-family: "Segoe UI", "Helvetica Neue", ui-sans-serif, system-ui, sans-serif;
|
|
38
57
|
color: var(--ink);
|
|
39
|
-
background:
|
|
40
|
-
radial-gradient(900px 480px at 12% -10%, var(--bg2), transparent 60%),
|
|
41
|
-
radial-gradient(700px 420px at 100% 0%, #e7efe4, transparent 55%),
|
|
42
|
-
linear-gradient(165deg, var(--bg0), var(--bg1) 55%, #eef2ef);
|
|
58
|
+
background: var(--bg);
|
|
43
59
|
display: grid;
|
|
44
60
|
place-items: center;
|
|
45
61
|
padding: 1.5rem;
|
|
@@ -51,7 +67,7 @@ function loginCallbackPage(ok) {
|
|
|
51
67
|
border-radius: 1.25rem;
|
|
52
68
|
padding: 2rem 1.75rem 1.75rem;
|
|
53
69
|
backdrop-filter: blur(10px);
|
|
54
|
-
box-shadow:
|
|
70
|
+
box-shadow: var(--shadow);
|
|
55
71
|
}
|
|
56
72
|
.mark {
|
|
57
73
|
width: 2.75rem;
|
|
@@ -60,7 +76,7 @@ function loginCallbackPage(ok) {
|
|
|
60
76
|
display: grid;
|
|
61
77
|
place-items: center;
|
|
62
78
|
background: var(--mark-bg);
|
|
63
|
-
color:
|
|
79
|
+
color: var(--mark-ink);
|
|
64
80
|
margin-bottom: 1.25rem;
|
|
65
81
|
}
|
|
66
82
|
.mark svg { width: 1.4rem; height: 1.4rem; display: block; }
|
|
@@ -98,7 +114,7 @@ function loginCallbackPage(ok) {
|
|
|
98
114
|
padding: 0.1em 0.35em;
|
|
99
115
|
border: 1px solid var(--line);
|
|
100
116
|
border-radius: 0.35rem;
|
|
101
|
-
background:
|
|
117
|
+
background: var(--field);
|
|
102
118
|
}
|
|
103
119
|
</style>
|
|
104
120
|
</head>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-DRp7sI6i.mjs";
|
|
2
2
|
import { n as reportErrors } from "./ui-2mrJcjYC.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import { cpSync, existsSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Local worker secrets for `canopy dev` — copy to .dev.vars (gitignored).
|
|
2
|
+
#
|
|
3
|
+
# Dashboard secrets (Settings → Secrets) are never downloaded: the platform
|
|
4
|
+
# can't hand a stored value back, and live keys don't belong on a laptop. Set
|
|
5
|
+
# local stand-ins here; the worker reads them as c.env.NAME, exactly as it
|
|
6
|
+
# reads the dashboard's in production. `canopy dev` prints which dashboard
|
|
7
|
+
# secret names this file is missing.
|
|
8
|
+
#
|
|
9
|
+
# MY_API_KEY=local-test-key
|
|
@@ -28,6 +28,10 @@ the [Canopy dashboard](https://dashboard.thanarie.dev).
|
|
|
28
28
|
`event.name`). It runs behind the Canopy gateway (HTTP requests arrive
|
|
29
29
|
authenticated) and reaches the platform through a service binding — same
|
|
30
30
|
SDK, same budgets and data isolation as the browser.
|
|
31
|
+
- `canopy dev` serves the worker as well: `/api/*` goes to
|
|
32
|
+
`worker/index.ts` with the same `env` it gets deployed, no build step. Tick a
|
|
33
|
+
schedule by hand with
|
|
34
|
+
`curl -X POST localhost:5173/__canopy/scheduled -d '{"name":"<schedule>"}'`.
|
|
31
35
|
- The SDK comes from npm (`@mediamonks/canopy`) — the browser's shared
|
|
32
36
|
instance lives in `src/canopy.ts`.
|
|
33
37
|
- `canopy dev` uses LIVE project data: read freely, but take a
|
|
@@ -50,7 +54,9 @@ the [Canopy dashboard](https://dashboard.thanarie.dev).
|
|
|
50
54
|
reaches this app or the Worker.
|
|
51
55
|
- Do not store secrets in the repo or SPA. Set them in the Canopy dashboard
|
|
52
56
|
(Settings → Secrets); the Worker reads them as `c.env.SECRET_NAME`
|
|
53
|
-
(Workers-shaped bindings — not `process.env`).
|
|
57
|
+
(Workers-shaped bindings — not `process.env`). Locally, put stand-in values
|
|
58
|
+
in `.dev.vars` (gitignored, see `.dev.vars.example`) — dashboard values are
|
|
59
|
+
never downloaded, and `canopy dev` prints which names are still missing. Platform access still goes
|
|
54
60
|
through the Canopy SDK (`canopy.auth`, `canopy.db`, `canopy.blob`,
|
|
55
61
|
`canopy.ai`) in both the browser and the Worker.
|
|
56
62
|
- The Worker is the only backend: no SSR, no long-running processes, no
|
|
@@ -5,7 +5,10 @@ backend Worker (`worker/`). Live (behind company login) at
|
|
|
5
5
|
https://{{slug}}.thanarie.dev after the first `canopy deploy`.
|
|
6
6
|
|
|
7
7
|
- `npm install` — once, then:
|
|
8
|
-
- `canopy dev` — develop locally against the live platform (real data!)
|
|
8
|
+
- `canopy dev` — develop locally against the live platform (real data!);
|
|
9
|
+
`/api/*` is served by `worker/index.ts`, everything else by the SPA
|
|
10
|
+
- `.dev.vars` — local values for the worker's secrets (copy
|
|
11
|
+
`.dev.vars.example`); dashboard secrets are never downloaded
|
|
9
12
|
- `canopy dev --local [--pull]` — offline sandbox with local data instead
|
|
10
13
|
- `canopy deploy` — build and publish the frontend and the `worker/` backend
|
|
11
14
|
- `AGENTS.md` — project guardrails and skill setup for coding agents
|
|
@@ -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-
|
|
115
|
+
const { skillInstalled } = await import("./skill-B2sDMAmv.mjs");
|
|
116
116
|
const refreshSkill = skillInstalled(process.cwd());
|
|
117
117
|
console.log(`Upgrading via ${command}: ${command} ${cmd.join(" ")}`);
|
|
118
118
|
await run(command, cmd);
|
package/package.json
CHANGED
package/dist/dev-CQq2uzSz.mjs
DELETED