@mediamonks/canopy-cli 0.7.28 → 0.7.29
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-DVMwCLlU.mjs → db-KTLn8J1u.mjs} +2 -2
- package/dist/{deploy-CTMcwHhI.mjs → deploy-CBf44bvG.mjs} +2 -2
- package/dist/dev-DGCu9MjB.mjs +2 -0
- package/dist/{dev-BvjbJcse.mjs → dev-JlzMZJow.mjs} +50 -21
- package/dist/{dev-worker-BerdPJ_U.mjs → dev-worker-DjJGSUGj.mjs} +1 -1
- package/dist/docs/cli-db.md +2 -0
- package/dist/docs/index.json +12 -2
- package/dist/docs/platform-members-and-access.md +13 -4
- package/dist/docs/platform-slack.md +6 -0
- package/dist/docs/sdk-notify.md +140 -0
- package/dist/docs/sdk-realtime.md +149 -0
- package/dist/docs/sdk-slack.md +3 -0
- package/dist/docs/sdk.md +5 -4
- package/dist/docs/static-serving.md +19 -0
- package/dist/{docs-DMFMPnxG.mjs → docs-ZQnhFSKT.mjs} +2 -2
- package/dist/{egress-Cn-P46O3.mjs → egress-Cn9zDfDT.mjs} +1 -1
- package/dist/{feedback-OMAkwHpR.mjs → feedback-CNfUjjNw.mjs} +2 -2
- package/dist/index.mjs +26 -26
- package/dist/{local-laAZEDVJ.mjs → local-BA_TPWpN.mjs} +56 -4
- package/dist/{login-C4MDP5NM.mjs → login-MX0xq8UE.mjs} +1 -1
- package/dist/{logs-CfDINw7U.mjs → logs-D5RWqsWl.mjs} +1 -1
- package/dist/{migrations-B9fXxMDj.mjs → migrations-C8yRPJIX.mjs} +1 -1
- package/dist/{open-rS1ARzOy.mjs → open-BJgd0R61.mjs} +1 -1
- package/dist/{package-BL6hRqtr.mjs → package-DzU_2IJi.mjs} +1 -1
- package/dist/{projects-Dmm_976k.mjs → projects-BG7Injpb.mjs} +1 -1
- package/dist/{schedule-Buu71RWQ.mjs → schedule-CLuksxSM.mjs} +1 -1
- package/dist/{secrets-BD3DaTih.mjs → secrets-Bl3uOx4l.mjs} +1 -1
- package/dist/{skill-C83Za5BU.mjs → skill-CQ4a__Ga.mjs} +2 -2
- package/dist/{src-CwSKZwNK.mjs → src-C7zPhbxi.mjs} +238 -1
- package/dist/{staging-CKEoDnac.mjs → staging-D06zobnf.mjs} +1 -1
- package/dist/{token-Dp2CoIYj.mjs → token-C_PFNqcv.mjs} +1 -1
- package/dist/{ui-DR-siu2S.mjs → ui-CeupNHNa.mjs} +4 -0
- package/dist/{upgrade-BjQguwG6.mjs → upgrade-BzmIrnjs.mjs} +2 -2
- package/dist/{usage-81JSiWIx.mjs → usage-CV7TjTO8.mjs} +1 -1
- package/package.json +2 -2
- package/dist/dev-BOvMCaML.mjs +0 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { c as resolveProject, o as projectFlagArgs } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import { a as unwrap, d as withContext, i as requireTty, l as apiRequest, r as reportErrors, s as apiDataRequest } from "./ui-
|
|
3
|
-
import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-
|
|
2
|
+
import { a as unwrap, d as withContext, i as requireTty, l as apiRequest, r as reportErrors, s as apiDataRequest } from "./ui-CeupNHNa.mjs";
|
|
3
|
+
import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-C8yRPJIX.mjs";
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
5
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
6
6
|
import * as p from "@clack/prompts";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
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-DM2J10CB.mjs";
|
|
2
|
-
import { a as unwrap, c as apiDownload, d as withContext, l as apiRequest, r as reportErrors, s as apiDataRequest, t as formatDateTime } from "./ui-
|
|
3
|
-
import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-
|
|
2
|
+
import { a as unwrap, c as apiDownload, d as withContext, l as apiRequest, r as reportErrors, s as apiDataRequest, t as formatDateTime } from "./ui-CeupNHNa.mjs";
|
|
3
|
+
import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-C8yRPJIX.mjs";
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
5
|
import { mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { a as projectDirArgs, c as resolveProject, i as projectDialect, n as siteConfig, u as tryReadCanopyJson } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import { d as withContext, l as apiRequest, o as ApiError, r as reportErrors, t as formatDateTime } from "./ui-
|
|
3
|
-
import { r as readMigrationFiles } from "./migrations-
|
|
4
|
-
import { n as stampWorkerHeaders } from "./src-
|
|
2
|
+
import { d as withContext, l as apiRequest, o as ApiError, r as reportErrors, t as formatDateTime } from "./ui-CeupNHNa.mjs";
|
|
3
|
+
import { r as readMigrationFiles } from "./migrations-C8yRPJIX.mjs";
|
|
4
|
+
import { n as stampWorkerHeaders } from "./src-C7zPhbxi.mjs";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { defineCommand } from "citty";
|
|
7
7
|
import { existsSync } from "node:fs";
|
|
@@ -38,7 +38,7 @@ async function workerPlugin(flags, slug) {
|
|
|
38
38
|
console.log(`note: canopy.json points "worker" at ${entry}, which does not exist — skipping.`);
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
|
-
const { missingSecretNames, readDevVars, workerDevPlugin } = await import("./dev-worker-
|
|
41
|
+
const { missingSecretNames, readDevVars, workerDevPlugin } = await import("./dev-worker-DjJGSUGj.mjs");
|
|
42
42
|
const secrets = readDevVars(flags.dir);
|
|
43
43
|
const email = siteConfig().email;
|
|
44
44
|
const plugin = workerDevPlugin({
|
|
@@ -146,6 +146,48 @@ function devProxyHeaders(token, slug, channel) {
|
|
|
146
146
|
});
|
|
147
147
|
return Object.fromEntries(headers);
|
|
148
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* One request from Vite's /_api proxy into the --local sandbox. The response
|
|
151
|
+
* body is streamed, not buffered — realtime subscriptions (docs/48) are
|
|
152
|
+
* long-lived SSE responses — and a client that goes away aborts the sandbox
|
|
153
|
+
* request and cancels its body, so the subscription is released.
|
|
154
|
+
*/
|
|
155
|
+
async function bridgeToSandbox(sandbox, req, res) {
|
|
156
|
+
const gone = new AbortController();
|
|
157
|
+
res.on("close", () => gone.abort());
|
|
158
|
+
const chunks = [];
|
|
159
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
160
|
+
const body = Buffer.concat(chunks);
|
|
161
|
+
const headers = new Headers();
|
|
162
|
+
if (req.headers["content-type"]) headers.set("content-type", req.headers["content-type"]);
|
|
163
|
+
if (req.headers["accept"]) headers.set("accept", req.headers["accept"]);
|
|
164
|
+
const response = await sandbox.handle(new Request(`http://canopy.local${req.url ?? ""}`, {
|
|
165
|
+
method: req.method,
|
|
166
|
+
headers,
|
|
167
|
+
body: body.length === 0 ? void 0 : new Uint8Array(body),
|
|
168
|
+
signal: gone.signal
|
|
169
|
+
}));
|
|
170
|
+
if (gone.signal.aborted) {
|
|
171
|
+
await response.body?.cancel().catch(() => {});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
res.writeHead(response.status, Object.fromEntries(response.headers));
|
|
175
|
+
if (!response.body) {
|
|
176
|
+
res.end();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const reader = response.body.getReader();
|
|
180
|
+
gone.signal.addEventListener("abort", () => void reader.cancel().catch(() => {}), { once: true });
|
|
181
|
+
try {
|
|
182
|
+
for (;;) {
|
|
183
|
+
const { done, value } = await reader.read();
|
|
184
|
+
if (done) break;
|
|
185
|
+
res.write(value);
|
|
186
|
+
}
|
|
187
|
+
} catch {} finally {
|
|
188
|
+
res.end();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
149
191
|
async function devRemote(flags, slug) {
|
|
150
192
|
const { token, expires_at } = await withContext(apiRequest("POST", `/projects/${slug}/dev-token`), devTokenContext(slug));
|
|
151
193
|
const { apiUrl } = siteConfig();
|
|
@@ -176,7 +218,7 @@ async function devRemote(flags, slug) {
|
|
|
176
218
|
async function devLocal(flags, slug) {
|
|
177
219
|
let dump;
|
|
178
220
|
if (flags.pull) {
|
|
179
|
-
const { fetchProjectDump } = await import("./local-
|
|
221
|
+
const { fetchProjectDump } = await import("./local-BA_TPWpN.mjs");
|
|
180
222
|
console.log(`Pulling a fresh export of ${slug}${flags.staging ? " [staging]" : ""}…`);
|
|
181
223
|
dump = await fetchProjectDump(slug, flags.staging ? "staging" : "live");
|
|
182
224
|
}
|
|
@@ -191,7 +233,7 @@ async function devLocal(flags, slug) {
|
|
|
191
233
|
const reason = err instanceof ApiError ? err.explain().why : String(err);
|
|
192
234
|
console.log(`Note: ai/services will be unavailable — could not reach the platform (${reason}).`);
|
|
193
235
|
}
|
|
194
|
-
const { createSandbox } = await import("./local-
|
|
236
|
+
const { createSandbox } = await import("./local-BA_TPWpN.mjs");
|
|
195
237
|
const sandbox = await createSandbox({
|
|
196
238
|
slug,
|
|
197
239
|
persistDir: join(flags.dir, ".canopy", "local"),
|
|
@@ -207,20 +249,7 @@ async function devLocal(flags, slug) {
|
|
|
207
249
|
const { applied } = await sandbox.applyMigrations(migrationFiles);
|
|
208
250
|
if (applied.length > 0) console.log(`Applied ${applied.length} pending migration(s): ${applied.join(", ")}`);
|
|
209
251
|
}
|
|
210
|
-
const handleApi =
|
|
211
|
-
const chunks = [];
|
|
212
|
-
for await (const chunk of req) chunks.push(chunk);
|
|
213
|
-
const body = Buffer.concat(chunks);
|
|
214
|
-
const headers = new Headers();
|
|
215
|
-
if (req.headers["content-type"]) headers.set("content-type", req.headers["content-type"]);
|
|
216
|
-
const response = await sandbox.handle(new Request(`http://canopy.local${req.url ?? ""}`, {
|
|
217
|
-
method: req.method,
|
|
218
|
-
headers,
|
|
219
|
-
body: body.length === 0 ? void 0 : new Uint8Array(body)
|
|
220
|
-
}));
|
|
221
|
-
res.writeHead(response.status, Object.fromEntries(response.headers));
|
|
222
|
-
res.end(Buffer.from(await response.arrayBuffer()));
|
|
223
|
-
};
|
|
252
|
+
const handleApi = (req, res) => bridgeToSandbox(sandbox, req, res);
|
|
224
253
|
const { createServer: createHttpServer } = await import("node:http");
|
|
225
254
|
const apiServer = createHttpServer((req, res) => {
|
|
226
255
|
handleApi(req, res).catch((err) => {
|
|
@@ -312,4 +341,4 @@ const devCommand = defineCommand({
|
|
|
312
341
|
})
|
|
313
342
|
});
|
|
314
343
|
//#endregion
|
|
315
|
-
export {
|
|
344
|
+
export { rewriteApiPath as i, devCommand as n, devProxyHeaders as r, bridgeToSandbox as t };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as stampWorkerHeaders, t as redactCanopyCredentials } from "./src-
|
|
1
|
+
import { n as stampWorkerHeaders, t as redactCanopyCredentials } from "./src-C7zPhbxi.mjs";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { relative, resolve, sep } from "node:path";
|
|
4
4
|
//#region src/dev-worker.ts
|
package/dist/docs/cli-db.md
CHANGED
|
@@ -53,6 +53,8 @@ Behavior:
|
|
|
53
53
|
2. Unless `--yes` is passed, it asks you to **type the project slug** to confirm — a deliberate speed bump, since restore is destructive. A non-TTY without `--yes` errors instead of guessing.
|
|
54
54
|
3. The chosen snapshot or file replaces the entire database, and the CLI confirms `Restored <slug>.`
|
|
55
55
|
|
|
56
|
+
A dump only restores into a database of the same kind. Feeding a dump from a SQLite-based project to a Postgres-based one (or the other way round) is refused up front with `dump_dialect_mismatch`, before anything is wiped. Check with `canopy db pull` on both sides: a SQLite dump opens with a `PRAGMA` line, a Postgres dump with a `-- Canopy project dump (postgres)` banner. For a project that uses **document collections only**, the collection table has the same layout everywhere, so deleting that leading `PRAGMA` line makes a SQLite dump restore on Postgres. Custom tables need their column types adapted by hand.
|
|
57
|
+
|
|
56
58
|
### canopy db migrate
|
|
57
59
|
|
|
58
60
|
Applies pending schema migrations to the project's **live** database, without deploying. Requires a `"migrations"` directory in `canopy.json` (for example your `drizzle-kit generate` output — see the [migrations guide](https://docs.canopy.website/guides/migrations)).
|
package/dist/docs/index.json
CHANGED
|
@@ -306,12 +306,12 @@
|
|
|
306
306
|
{
|
|
307
307
|
"key": "sdk",
|
|
308
308
|
"title": "SDK reference",
|
|
309
|
-
"description": "The typed client your project code uses — auth, data, files, AI, services.",
|
|
309
|
+
"description": "The typed client your project code uses — auth, data, files, AI, email, notify, realtime, services.",
|
|
310
310
|
"topics": [
|
|
311
311
|
{
|
|
312
312
|
"slug": "sdk",
|
|
313
313
|
"title": "Using the SDK",
|
|
314
|
-
"summary": "One typed client for auth, data, files, AI, email, and services — no keys, no connection strings, the platform authenticates for you."
|
|
314
|
+
"summary": "One typed client for auth, data, files, AI, email, notify, realtime, and services — no keys, no connection strings, the platform authenticates for you."
|
|
315
315
|
},
|
|
316
316
|
{
|
|
317
317
|
"slug": "sdk-auth",
|
|
@@ -338,6 +338,16 @@
|
|
|
338
338
|
"title": "Email",
|
|
339
339
|
"summary": "Send email from your project without a mail server or an API key — Monks recipients only, with a per-project daily quota and a sent log."
|
|
340
340
|
},
|
|
341
|
+
{
|
|
342
|
+
"slug": "sdk-notify",
|
|
343
|
+
"title": "Notify",
|
|
344
|
+
"summary": "Post a Slack message from your project through the Canopy bot — to your linked channels or a colleague's DMs — with nothing to set up."
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
"slug": "sdk-realtime",
|
|
348
|
+
"title": "Realtime",
|
|
349
|
+
"summary": "Live updates between the people using your app — subscribe to a topic, publish to it, no backend needed."
|
|
350
|
+
},
|
|
341
351
|
{
|
|
342
352
|
"slug": "sdk-google",
|
|
343
353
|
"title": "Google",
|
|
@@ -35,10 +35,19 @@ nobody outside the team can touch its data or settings.
|
|
|
35
35
|
|
|
36
36
|
## Inviting collaborators
|
|
37
37
|
|
|
38
|
-
Open the project's **Settings** tab and use the **Collaborators** card.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
Open the project's **Settings** tab and use the **Collaborators** card. Start
|
|
39
|
+
typing a name or email: colleagues who have signed in to Canopy before show up
|
|
40
|
+
as suggestions, and picking one adds them immediately.
|
|
41
|
+
|
|
42
|
+
Someone who has never opened Canopy can be added too — type their full
|
|
43
|
+
company email and choose **Invite by email**. They appear on the roster as
|
|
44
|
+
**invited** and become a collaborator automatically the first time they sign
|
|
45
|
+
in, whether that's opening the project's URL or running `canopy login`. Until
|
|
46
|
+
then the invite grants nothing. You can withdraw a pending invite from the
|
|
47
|
+
same card. Canopy doesn't send them a message, so let them know yourself.
|
|
48
|
+
|
|
49
|
+
Only company addresses that can sign in to Canopy can be invited. Owners (and
|
|
50
|
+
admins) can also remove people from the same card.
|
|
42
51
|
|
|
43
52
|
## Collaborators-only mode
|
|
44
53
|
|
|
@@ -3,6 +3,12 @@ title: Slack
|
|
|
3
3
|
description: Give your project its own Slack bot — Canopy creates and manages the app, your code handles the events.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
> **A bot, or just a message?** If all your project needs is to post to a
|
|
7
|
+
> channel or DM a colleague, skip this page: `canopy.notify.slack` does that
|
|
8
|
+
> through the Canopy bot with nothing to set up. Read
|
|
9
|
+
> [Notify](https://docs.canopy.website/sdk/notify). Come back here when people need to talk to your
|
|
10
|
+
> bot, run slash commands, or see your project's own name and icon.
|
|
11
|
+
|
|
6
12
|
Your project can have its own Slack app: its own name and bot, in whatever
|
|
7
13
|
workspace you choose. Canopy creates the app, keeps its settings in sync,
|
|
8
14
|
holds every credential, and routes Slack's traffic to your backend. Your
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Notify
|
|
3
|
+
description: Post a Slack message from your project through the Canopy bot — to your linked channels or a colleague's DMs — with nothing to set up.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`canopy.notify` tells people things on your project's behalf. The Canopy bot,
|
|
7
|
+
the one that already DMs you about reports and deploys, delivers the message
|
|
8
|
+
and says which project sent it. There is no Slack app to create, no token, no
|
|
9
|
+
install: it works the moment your project has something to say.
|
|
10
|
+
|
|
11
|
+
Two rules are enforced by the platform, not by your code:
|
|
12
|
+
|
|
13
|
+
- **You reach people your project has a relationship with.** Channels are the
|
|
14
|
+
ones a project member linked with `/canopy link`; people are Canopy users,
|
|
15
|
+
named by the email they sign in with. Nothing else is addressable.
|
|
16
|
+
- **A daily quota per project**, 200 deliveries by default. Each person and
|
|
17
|
+
each channel a message goes to counts as one.
|
|
18
|
+
|
|
19
|
+
## notify.slack(options)
|
|
20
|
+
|
|
21
|
+
Resolves once the platform has queued the message:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// Every channel linked to the project.
|
|
25
|
+
await canopy.notify.slack({ text: "Export finished: *1,204* rows." });
|
|
26
|
+
|
|
27
|
+
// A colleague, and nobody else.
|
|
28
|
+
await canopy.notify.slack({
|
|
29
|
+
to: "someone@monks.com",
|
|
30
|
+
text: "Your approval is needed on <https://…|request #42>.",
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// People and the linked channels together.
|
|
34
|
+
await canopy.notify.slack({ to: ["a@monks.com", "b@monks.com"], channels: true, text: "…" });
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**`NotifySlackOptions`:**
|
|
38
|
+
|
|
39
|
+
- `text` — required, up to 2000 characters of Slack mrkdwn. Bold, links and
|
|
40
|
+
`<@U…>` mentions work.
|
|
41
|
+
- `to` — optional. One email or an array of up to 20, each the address a
|
|
42
|
+
Canopy user signs in with. Leave it out to post to the linked channels
|
|
43
|
+
instead.
|
|
44
|
+
- `channels` — optional. With `to`, also post to the linked channels. Without
|
|
45
|
+
`to` it is always on.
|
|
46
|
+
|
|
47
|
+
**Returns** `{ queued }`, the number of deliveries queued: people plus
|
|
48
|
+
channels. Delivery happens right after, with retries if Slack is slow; the
|
|
49
|
+
outcome shows on the project's Usage tab.
|
|
50
|
+
|
|
51
|
+
## What the message looks like
|
|
52
|
+
|
|
53
|
+
Every message opens with a line the platform writes: your project's name,
|
|
54
|
+
linked to its dashboard page, and "sent by this project via Canopy". Your
|
|
55
|
+
text follows as a new top-level message. Nothing threads onto earlier
|
|
56
|
+
messages, and there are no buttons or blocks: this is a notification, not a
|
|
57
|
+
conversation.
|
|
58
|
+
|
|
59
|
+
If someone replies under it, the Canopy assistant answers that the message
|
|
60
|
+
came from your project and that replies don't reach it. If you want replies
|
|
61
|
+
to reach your code, you want [a bot of your own](https://docs.canopy.website/platform/slack).
|
|
62
|
+
|
|
63
|
+
## Who receives it
|
|
64
|
+
|
|
65
|
+
Channels get every message. People are protected the same way platform
|
|
66
|
+
notifications protect them: a colleague who muted your project hears
|
|
67
|
+
nothing, and anyone can turn off "Messages from projects" in their
|
|
68
|
+
notification settings. Someone whose email has no Slack account is skipped.
|
|
69
|
+
None of this is reported back to your code, so `queued` counts what was
|
|
70
|
+
sent, not what arrived.
|
|
71
|
+
|
|
72
|
+
An email that is not a Canopy user is refused outright, so you notice the
|
|
73
|
+
typo. Everyone at Monks becomes a Canopy user by opening the dashboard once.
|
|
74
|
+
|
|
75
|
+
## Linking a channel
|
|
76
|
+
|
|
77
|
+
Type `/canopy link <your-project-slug>` in the channel, then invite `@Canopy`
|
|
78
|
+
to it. The channel now receives the project's messages, and its activity
|
|
79
|
+
too: reports, project events, deploys. Linked channels are listed under the
|
|
80
|
+
project's **Services → Slack**, where any member can unlink one.
|
|
81
|
+
|
|
82
|
+
Until a channel is linked, a send without `to` fails with a 404 rather than
|
|
83
|
+
disappearing silently.
|
|
84
|
+
|
|
85
|
+
## Sending from the backend
|
|
86
|
+
|
|
87
|
+
Like email, this is meant for code that runs with nobody signed in: a
|
|
88
|
+
[scheduled](https://docs.canopy.website/platform/schedules) job, a worker route reacting to a webhook.
|
|
89
|
+
It authenticates as the project itself. Browser code cannot call it.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
export default {
|
|
93
|
+
async scheduled(event, env) {
|
|
94
|
+
const canopy = createCanopy({ fetch: env.CANOPY.fetch.bind(env.CANOPY) });
|
|
95
|
+
const stale = await staleRequests(env.DB);
|
|
96
|
+
if (stale.length === 0) return;
|
|
97
|
+
|
|
98
|
+
await canopy.notify.slack({
|
|
99
|
+
text: `${stale.length} approval request${stale.length === 1 ? "" : "s"} waiting longer than a week.`,
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Seeing what was sent
|
|
106
|
+
|
|
107
|
+
The project's **Usage** tab shows today's count against the quota and the
|
|
108
|
+
recent sends: when, to whom, the first line, and whether it was delivered.
|
|
109
|
+
Only that first line is kept.
|
|
110
|
+
|
|
111
|
+
## When it fails
|
|
112
|
+
|
|
113
|
+
Every failure is a `CanopyError` with a status you can branch on:
|
|
114
|
+
|
|
115
|
+
- **400** — the text is empty or too long, `to` has more than 20 entries, or
|
|
116
|
+
an address is not a Canopy user (`unknown_recipient`, naming it).
|
|
117
|
+
- **404** — no `to` and no linked channel (`no_linked_channel`).
|
|
118
|
+
- **429** — the project used its daily quota, or the platform-wide ceiling is
|
|
119
|
+
reached. Both reset at UTC midnight.
|
|
120
|
+
- **501** — this Canopy site has no Canopy bot.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import { CanopyError } from "@mediamonks/canopy";
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
await canopy.notify.slack({ text });
|
|
127
|
+
} catch (err) {
|
|
128
|
+
if (err instanceof CanopyError && err.status === 404) {
|
|
129
|
+
// Nobody has linked a channel yet — tell a member to run /canopy link.
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## What this is not
|
|
135
|
+
|
|
136
|
+
This is not a bot. It cannot listen, answer, show buttons, run slash
|
|
137
|
+
commands, post in a client's workspace or carry your project's name and
|
|
138
|
+
icon. The moment you want any of that, give the project [its own Slack
|
|
139
|
+
app](https://docs.canopy.website/platform/slack): a few minutes of setup, and `canopy.slack` does the
|
|
140
|
+
rest.
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Realtime
|
|
3
|
+
description: Live updates between the people using your app — subscribe to a topic, publish to it, no backend needed.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`canopy.realtime` sends small messages to everyone who has your app open,
|
|
7
|
+
the moment something happens. A teammate adds a card and everyone's board
|
|
8
|
+
refreshes; someone starts editing and the others see a "being edited" badge.
|
|
9
|
+
It works from the browser alone, with nothing to set up.
|
|
10
|
+
|
|
11
|
+
## Messages are hints, not a log
|
|
12
|
+
|
|
13
|
+
Nothing is stored. A message reaches the pages that are connected when it's
|
|
14
|
+
sent, and nobody else. A laptop that went to sleep, a tab that was in the
|
|
15
|
+
background, a page that reconnected: each of them missed whatever was sent
|
|
16
|
+
meanwhile.
|
|
17
|
+
|
|
18
|
+
So build it like this: keep your data in [`canopy.db`](https://docs.canopy.website/sdk/database),
|
|
19
|
+
send a message to say _what changed_, and reload from the database whenever
|
|
20
|
+
the connection opens.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
const off = canopy.realtime.subscribe(
|
|
24
|
+
"board",
|
|
25
|
+
(message) => refreshCard(message.data.cardId),
|
|
26
|
+
{ onOpen: () => reloadBoard() }, // first connect and every reconnect
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// …somewhere else, after saving the change:
|
|
30
|
+
await canopy.db.collection("cards").update(card.id, { column: "done" });
|
|
31
|
+
await canopy.realtime.publish("board", { cardId: card.id });
|
|
32
|
+
|
|
33
|
+
off(); // stop listening
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## realtime.subscribe(topic, onMessage, options?)
|
|
37
|
+
|
|
38
|
+
Starts listening to `topic` and returns a function that stops. Every
|
|
39
|
+
subscription on a page shares one connection, so subscribe to as many topics
|
|
40
|
+
as you need (up to 20 per page).
|
|
41
|
+
|
|
42
|
+
- `onMessage(message)` runs for each message on the topic, including the ones
|
|
43
|
+
this page published itself. Ignore your own by comparing `message.id` with
|
|
44
|
+
what `publish` returned.
|
|
45
|
+
- `onOpen()` runs whenever the connection opens: the first time, and after
|
|
46
|
+
every reconnect. Reload what you show from the database here.
|
|
47
|
+
- `onError(error)` reports connection problems. `error.code` says which. The
|
|
48
|
+
SDK keeps retrying on its own unless retrying can't help (see
|
|
49
|
+
[When it's off](#when-its-off)).
|
|
50
|
+
|
|
51
|
+
A message looks like this:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
{
|
|
55
|
+
id: "0muz…", // the id publish() returned to the sender
|
|
56
|
+
topic: "board",
|
|
57
|
+
data: { cardId: "c-7" }, // whatever was published
|
|
58
|
+
from: { kind: "monk", email: "ada@monks.com" },
|
|
59
|
+
ts: "2026-10-08T12:00:00.000Z",
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`from` is filled in by Canopy, never by the sender, so you can trust it:
|
|
64
|
+
`monk` (with their sign-in email), `external` for a visitor who came in with
|
|
65
|
+
the [client password](https://docs.canopy.website/platform/client-access), or `worker` for your
|
|
66
|
+
project's backend. Client visitors never see a monk's email; for them the
|
|
67
|
+
sender is just `monk`.
|
|
68
|
+
|
|
69
|
+
## realtime.publish(topic, data)
|
|
70
|
+
|
|
71
|
+
Sends `data` (any JSON value, up to 4 KB) to everyone subscribed to `topic`,
|
|
72
|
+
and resolves with `{ id }`. Send an id and let pages load the rest from the
|
|
73
|
+
database; a message over 4 KB is refused with `message_too_large`.
|
|
74
|
+
|
|
75
|
+
Topic names are 1 to 64 letters, digits, `-`, `_` or `.`. They belong to your
|
|
76
|
+
project only: another project's `board` is a different topic, and so is your
|
|
77
|
+
own staging site's.
|
|
78
|
+
|
|
79
|
+
## Topics only your backend can publish to
|
|
80
|
+
|
|
81
|
+
Topics that start with `worker:` take messages from your project's backend
|
|
82
|
+
only. Pages can listen to them, but anything else that tries to publish gets
|
|
83
|
+
`topic_worker_only`: a page, the dashboard, or a personal token like the one
|
|
84
|
+
`canopy dev` uses. Use them for news that has to come from the server: a
|
|
85
|
+
job finished, a payment cleared, an import is done.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
// In your worker (worker/index.ts):
|
|
89
|
+
const canopy = createCanopy({ fetch: c.env.CANOPY.fetch.bind(c.env.CANOPY) });
|
|
90
|
+
await canopy.realtime.publish("worker:imports", { importId, status: "done" });
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Messages from the backend arrive with `from.kind: "worker"`, also when the
|
|
94
|
+
backend is handling a signed-in visitor's request. A scheduled job can
|
|
95
|
+
publish too.
|
|
96
|
+
|
|
97
|
+
## Limits
|
|
98
|
+
|
|
99
|
+
- **4 KB** of data per message.
|
|
100
|
+
- **20 topics** per page.
|
|
101
|
+
- **100 open connections** per project at a time, and **10 per person**. One
|
|
102
|
+
more gets `too_many_subscribers` through `onError` and keeps retrying until
|
|
103
|
+
a connection closes.
|
|
104
|
+
- **A daily quota** per project: 100,000 messages by default, and a share of
|
|
105
|
+
data delivered (big messages to many people use it up faster). When either
|
|
106
|
+
is used up, `publish` is refused with `realtime_quota_exceeded` until
|
|
107
|
+
midnight UTC; subscriptions stay open. The Usage tab shows today's count. A
|
|
108
|
+
Canopy admin can raise the message quota if you need more.
|
|
109
|
+
- If many projects are busy at once, the whole site can reach its own daily
|
|
110
|
+
limit. Publishes then fail with `realtime_platform_limit`; that one isn't
|
|
111
|
+
about your project.
|
|
112
|
+
|
|
113
|
+
For cursors or presence, throttle updates to at most a few per second per
|
|
114
|
+
person. That's smooth to look at and keeps you well inside the quota.
|
|
115
|
+
|
|
116
|
+
## Background tabs
|
|
117
|
+
|
|
118
|
+
A tab that has been in the background for two minutes stops receiving
|
|
119
|
+
messages and lets its connection go. When you switch back, it reconnects
|
|
120
|
+
right away and `onOpen` runs, so the page catches up from the database. A
|
|
121
|
+
quick switch away and back keeps the connection.
|
|
122
|
+
|
|
123
|
+
Every connection also renews itself about every quarter of an hour, and
|
|
124
|
+
whenever access to the project changes (the client password, collaborators
|
|
125
|
+
only, someone removed). Each renewal runs `onOpen` again, which is what
|
|
126
|
+
keeps the reload-on-open pattern correct.
|
|
127
|
+
|
|
128
|
+
## When it's off
|
|
129
|
+
|
|
130
|
+
Realtime is a per-project setting that Canopy admins manage. If it's off for
|
|
131
|
+
your project, `subscribe` reports `realtime_not_enabled` through `onError`
|
|
132
|
+
once and `publish` is refused with the same code. Ask a Canopy admin to turn
|
|
133
|
+
it on. The Usage tab shows whether it's on.
|
|
134
|
+
|
|
135
|
+
Some Canopy sites don't run realtime at all; there the error is
|
|
136
|
+
`not_supported_on_target`. In both cases the SDK doesn't keep retrying.
|
|
137
|
+
|
|
138
|
+
## Topics are not secrets
|
|
139
|
+
|
|
140
|
+
Anyone who can open your app can subscribe to any of its topics. Don't put
|
|
141
|
+
anything in a message that the people using the app shouldn't see, and don't
|
|
142
|
+
treat a hard-to-guess topic name as a password.
|
|
143
|
+
|
|
144
|
+
## In local development
|
|
145
|
+
|
|
146
|
+
`canopy dev` connects to the real topics of your live project, so a local tab
|
|
147
|
+
and the deployed site see each other's messages. `canopy dev --local` keeps
|
|
148
|
+
everything on your laptop: open two tabs and they talk to each other, with no
|
|
149
|
+
quota and no `worker:` rule (like the local database, nothing is guarded).
|
package/dist/docs/sdk-slack.md
CHANGED
|
@@ -3,6 +3,9 @@ title: Slack
|
|
|
3
3
|
description: Handle your bot's events and slash commands in your worker, and post to Slack as the bot — no tokens in project code.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
> Only want to post a message? [`canopy.notify.slack`](https://docs.canopy.website/sdk/notify) does it
|
|
7
|
+
> through the Canopy bot, no app needed. This page is for a bot of your own.
|
|
8
|
+
|
|
6
9
|
Once your project has a Slack app ([set up on the Services
|
|
7
10
|
tab](https://docs.canopy.website/platform/slack)), the integration is two halves: inbound events arrive
|
|
8
11
|
at a `slack()` export in your worker, and outbound calls go through
|
package/dist/docs/sdk.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Using the SDK
|
|
3
|
-
description: One typed client for auth, data, files, AI, email, and services — no keys, no connection strings, the platform authenticates for you.
|
|
3
|
+
description: One typed client for auth, data, files, AI, email, notify, realtime, and services — no keys, no connection strings, the platform authenticates for you.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
`@mediamonks/canopy` is a thin, typed client over the platform's project API.
|
|
7
|
-
One `createCanopy()` call gives you
|
|
8
|
-
`email`, `google`, and `services` — and that is the entire surface. There are no API keys,
|
|
7
|
+
One `createCanopy()` call gives you its modules — `auth`, `db`, `blob`, `ai`,
|
|
8
|
+
`email`, `notify`, `realtime`, `google`, `slack`, and `services` — and that is the entire surface. There are no API keys,
|
|
9
9
|
connection strings, or environment variables to manage: every call rides the
|
|
10
10
|
session the platform already established before your code ran.
|
|
11
11
|
|
|
@@ -35,7 +35,8 @@ import { createCanopy } from "/_api/data/v1/sdk.js";
|
|
|
35
35
|
import { createCanopy } from "@mediamonks/canopy";
|
|
36
36
|
|
|
37
37
|
const canopy = createCanopy();
|
|
38
|
-
// canopy.auth, canopy.db, canopy.blob, canopy.ai, canopy.email, canopy.
|
|
38
|
+
// canopy.auth, canopy.db, canopy.blob, canopy.ai, canopy.email, canopy.notify,
|
|
39
|
+
// canopy.realtime, canopy.google, canopy.slack, canopy.services
|
|
39
40
|
```
|
|
40
41
|
|
|
41
42
|
| Option | Type | Default | What it does |
|
|
@@ -77,5 +77,24 @@ belong in [file storage](https://docs.canopy.website/sdk/files) instead of the b
|
|
|
77
77
|
reachable, deploy it, or put it in [file storage](https://docs.canopy.website/sdk/files) and hand out a
|
|
78
78
|
signed link.
|
|
79
79
|
|
|
80
|
+
## Moving a project to a new address
|
|
81
|
+
|
|
82
|
+
When a project lives somewhere else now — it was rebuilt under another name,
|
|
83
|
+
or it graduated to its own domain — you can point the old address at the new
|
|
84
|
+
one. On the project's **Settings** page, the **Move project** card takes an
|
|
85
|
+
`https://` URL. From then on every request to the old address, including deep
|
|
86
|
+
links with their path and query, lands on the new URL.
|
|
87
|
+
|
|
88
|
+
A few things to know:
|
|
89
|
+
|
|
90
|
+
- **Visitors still sign in on the old address first.** The redirect happens
|
|
91
|
+
behind the same sign-in wall as everything else, so a colleague signs in once
|
|
92
|
+
and is sent on; a client-password visitor types the old password once.
|
|
93
|
+
- **The old project goes quiet.** Deploys are refused, the backend and
|
|
94
|
+
schedules stop. Its database and files stay put, and `canopy db pull` still
|
|
95
|
+
works, so you can take the data with you after the move.
|
|
96
|
+
- **It's reversible.** Undo the move on the same card and the last deployed
|
|
97
|
+
build is served again right away.
|
|
98
|
+
|
|
80
99
|
Porting an app that assumed a server? [Porting a Next.js app](https://docs.canopy.website/guides/porting-nextjs)
|
|
81
100
|
walks through the restructure.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-DzU_2IJi.mjs";
|
|
2
2
|
import { n as siteConfig } from "./site-DM2J10CB.mjs";
|
|
3
3
|
import { i as latestKnownVersion, r as compareSemver } from "./version-check-CcsA1QjI.mjs";
|
|
4
|
-
import { r as reportErrors } from "./ui-
|
|
4
|
+
import { r as reportErrors } from "./ui-CeupNHNa.mjs";
|
|
5
5
|
import { defineCommand } from "citty";
|
|
6
6
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { homedir } from "node:os";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as projectDirArgs, c as resolveProject } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import { a as unwrap, d as withContext, i as requireTty, l as apiRequest, r as reportErrors, t as formatDateTime } from "./ui-
|
|
2
|
+
import { a as unwrap, d as withContext, i as requireTty, l as apiRequest, r as reportErrors, t as formatDateTime } from "./ui-CeupNHNa.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import * as p from "@clack/prompts";
|
|
5
5
|
//#region src/egress.ts
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-DzU_2IJi.mjs";
|
|
2
2
|
import { a as projectDirArgs, c as resolveProject, d as tryResolveProject, n as siteConfig, u as tryReadCanopyJson } from "./site-DM2J10CB.mjs";
|
|
3
|
-
import { c as apiDownload, l as apiRequest, r as reportErrors, t as formatDateTime, u as apiUpload } from "./ui-
|
|
3
|
+
import { c as apiDownload, l as apiRequest, r as reportErrors, t as formatDateTime, u as apiUpload } from "./ui-CeupNHNa.mjs";
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
5
|
import { basename, extname, join } from "node:path";
|
|
6
6
|
import { readFile, writeFile } from "node:fs/promises";
|
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-DzU_2IJi.mjs";
|
|
3
3
|
import { r as siteHeaderLine, t as setInvocationProfile } from "./site-DM2J10CB.mjs";
|
|
4
4
|
import { a as notifyOfUpdate, o as runRefresh } from "./version-check-CcsA1QjI.mjs";
|
|
5
5
|
import { defineCommand, runMain } from "citty";
|
|
@@ -60,31 +60,31 @@ 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-
|
|
66
|
-
create: () => import("./projects-
|
|
67
|
-
init: () => import("./projects-
|
|
68
|
-
projects: () => import("./projects-
|
|
69
|
-
delete: () => import("./projects-
|
|
70
|
-
dev: () => import("./dev-
|
|
71
|
-
deploy: () => import("./deploy-
|
|
72
|
-
promote: () => import("./deploy-
|
|
73
|
-
staging: () => import("./staging-
|
|
74
|
-
rollback: () => import("./deploy-
|
|
75
|
-
download: () => import("./deploy-
|
|
76
|
-
token: () => import("./token-
|
|
77
|
-
egress: () => import("./egress-
|
|
78
|
-
secrets: () => import("./secrets-
|
|
79
|
-
schedule: () => import("./schedule-
|
|
80
|
-
feedback: () => import("./feedback-
|
|
81
|
-
db: () => import("./db-
|
|
82
|
-
usage: () => import("./usage-
|
|
83
|
-
open: () => import("./open-
|
|
84
|
-
docs: () => import("./docs-
|
|
85
|
-
skill: () => import("./skill-
|
|
86
|
-
upgrade: () => import("./upgrade-
|
|
87
|
-
logs: () => import("./logs-
|
|
63
|
+
login: () => import("./login-MX0xq8UE.mjs").then((m) => m.loginCommand),
|
|
64
|
+
whoami: () => import("./login-MX0xq8UE.mjs").then((m) => m.whoamiCommand),
|
|
65
|
+
logout: () => import("./login-MX0xq8UE.mjs").then((m) => m.logoutCommand),
|
|
66
|
+
create: () => import("./projects-BG7Injpb.mjs").then((m) => m.createCommand),
|
|
67
|
+
init: () => import("./projects-BG7Injpb.mjs").then((m) => m.initCommand),
|
|
68
|
+
projects: () => import("./projects-BG7Injpb.mjs").then((m) => m.projectsCommand),
|
|
69
|
+
delete: () => import("./projects-BG7Injpb.mjs").then((m) => m.deleteCommand),
|
|
70
|
+
dev: () => import("./dev-DGCu9MjB.mjs").then((m) => m.devCommand),
|
|
71
|
+
deploy: () => import("./deploy-CBf44bvG.mjs").then((m) => m.deployCommand),
|
|
72
|
+
promote: () => import("./deploy-CBf44bvG.mjs").then((m) => m.promoteCommand),
|
|
73
|
+
staging: () => import("./staging-D06zobnf.mjs").then((m) => m.stagingCommand),
|
|
74
|
+
rollback: () => import("./deploy-CBf44bvG.mjs").then((m) => m.rollbackCommand),
|
|
75
|
+
download: () => import("./deploy-CBf44bvG.mjs").then((m) => m.downloadCommand),
|
|
76
|
+
token: () => import("./token-C_PFNqcv.mjs").then((m) => m.tokenCommand),
|
|
77
|
+
egress: () => import("./egress-Cn9zDfDT.mjs").then((m) => m.egressCommand),
|
|
78
|
+
secrets: () => import("./secrets-Bl3uOx4l.mjs").then((m) => m.secretsCommand),
|
|
79
|
+
schedule: () => import("./schedule-CLuksxSM.mjs").then((m) => m.scheduleCommand),
|
|
80
|
+
feedback: () => import("./feedback-CNfUjjNw.mjs").then((m) => m.feedbackCommand),
|
|
81
|
+
db: () => import("./db-KTLn8J1u.mjs").then((m) => m.dbCommand),
|
|
82
|
+
usage: () => import("./usage-CV7TjTO8.mjs").then((m) => m.usageCommand),
|
|
83
|
+
open: () => import("./open-BJgd0R61.mjs").then((m) => m.openCommand),
|
|
84
|
+
docs: () => import("./docs-ZQnhFSKT.mjs").then((m) => m.docsCommand),
|
|
85
|
+
skill: () => import("./skill-CQ4a__Ga.mjs").then((m) => m.skillCommand),
|
|
86
|
+
upgrade: () => import("./upgrade-BzmIrnjs.mjs").then((m) => m.upgradeCommand),
|
|
87
|
+
logs: () => import("./logs-D5RWqsWl.mjs").then((m) => m.logsCommand)
|
|
88
88
|
}
|
|
89
89
|
});
|
|
90
90
|
if (process.argv[2] !== "upgrade") notifyOfUpdate(version);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { s as apiDataRequest } from "./ui-
|
|
2
|
-
import { a as
|
|
3
|
-
import {
|
|
1
|
+
import { s as apiDataRequest } from "./ui-CeupNHNa.mjs";
|
|
2
|
+
import { a as META_COLUMNS, i as dbRouter, o as blobRouter, r as realtimeRouter } from "./src-C7zPhbxi.mjs";
|
|
3
|
+
import { i as rewriteApiPath } from "./dev-JlzMZJow.mjs";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { dirname, join, relative, sep } from "node:path";
|
|
@@ -532,6 +532,39 @@ async function applyMigrations(store, dialect, files) {
|
|
|
532
532
|
};
|
|
533
533
|
}
|
|
534
534
|
//#endregion
|
|
535
|
+
//#region ../data-plane/src/realtime-memory.ts
|
|
536
|
+
const sameScope = (a, b) => a.projectId === b.projectId && a.channel === b.channel;
|
|
537
|
+
function memoryRealtimeBroker() {
|
|
538
|
+
const subscriptions = /* @__PURE__ */ new Set();
|
|
539
|
+
const dropProject = (projectId) => {
|
|
540
|
+
for (const sub of subscriptions) {
|
|
541
|
+
if (sub.scope.projectId !== projectId) continue;
|
|
542
|
+
subscriptions.delete(sub);
|
|
543
|
+
sub.sink.close();
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
return {
|
|
547
|
+
async subscribe(scope, topics, sink) {
|
|
548
|
+
const sub = {
|
|
549
|
+
scope,
|
|
550
|
+
topics: new Set(topics),
|
|
551
|
+
sink
|
|
552
|
+
};
|
|
553
|
+
subscriptions.add(sub);
|
|
554
|
+
return { close: () => void subscriptions.delete(sub) };
|
|
555
|
+
},
|
|
556
|
+
async publish(scope, envelope) {
|
|
557
|
+
for (const sub of subscriptions) if (sameScope(sub.scope, scope) && sub.topics.has(envelope.topic)) sub.sink.deliver(envelope);
|
|
558
|
+
},
|
|
559
|
+
async disconnect(projectId) {
|
|
560
|
+
dropProject(projectId);
|
|
561
|
+
},
|
|
562
|
+
async purge(projectId) {
|
|
563
|
+
dropProject(projectId);
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
//#endregion
|
|
535
568
|
//#region src/fs-blob.ts
|
|
536
569
|
/**
|
|
537
570
|
* Filesystem BlobBucket for `canopy dev --local` — same structural interface
|
|
@@ -808,6 +841,22 @@ async function createSandbox(options) {
|
|
|
808
841
|
},
|
|
809
842
|
signDownload: async () => "local"
|
|
810
843
|
}));
|
|
844
|
+
const realtimeHub = memoryRealtimeBroker();
|
|
845
|
+
app.route("/data/v1", realtimeRouter({ context: async (c) => ownProject(c) ?? {
|
|
846
|
+
broker: realtimeHub,
|
|
847
|
+
enabled: true,
|
|
848
|
+
scope: {
|
|
849
|
+
projectId: options.slug,
|
|
850
|
+
channel: "live"
|
|
851
|
+
},
|
|
852
|
+
sender: {
|
|
853
|
+
kind: "monk",
|
|
854
|
+
email: options.email ?? "dev@localhost"
|
|
855
|
+
},
|
|
856
|
+
redactEmails: false,
|
|
857
|
+
isRuntime: true,
|
|
858
|
+
owner: "local"
|
|
859
|
+
} }));
|
|
811
860
|
app.get("/data/v1/sdk.js", () => {
|
|
812
861
|
return new Response(readFileSync(resolveSdkBundle(), "utf8"), { headers: {
|
|
813
862
|
"content-type": "text/javascript",
|
|
@@ -816,7 +865,10 @@ async function createSandbox(options) {
|
|
|
816
865
|
});
|
|
817
866
|
app.get("/data/v1/platform", (c) => c.json({
|
|
818
867
|
dialect: dialect.name,
|
|
819
|
-
capabilities: {
|
|
868
|
+
capabilities: {
|
|
869
|
+
fts: dialect.fts,
|
|
870
|
+
realtime: true
|
|
871
|
+
}
|
|
820
872
|
}));
|
|
821
873
|
app.get("/data/v1/auth/me", (c) => c.json({
|
|
822
874
|
id: "local-dev",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { _ as writeConfigFile, f as clearAllProfileTokens, g as upsertProfileAuth, h as setActiveProfile, m as readConfigFile, n as siteConfig, p as listBakedProfiles, v as getBakedProfile, y as isProfileId } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import { n as openBrowser, r as reportErrors } from "./ui-
|
|
2
|
+
import { n as openBrowser, r as reportErrors } from "./ui-CeupNHNa.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import { hostname } from "node:os";
|
|
5
5
|
import * as p from "@clack/prompts";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { c as resolveProject, o as projectFlagArgs } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import { d as withContext, l as apiRequest, r as reportErrors } from "./ui-
|
|
2
|
+
import { d as withContext, l as apiRequest, r as reportErrors } from "./ui-CeupNHNa.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
//#region src/logs.ts
|
|
5
5
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { u as tryReadCanopyJson } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import { s as apiDataRequest } from "./ui-
|
|
2
|
+
import { s as apiDataRequest } from "./ui-CeupNHNa.mjs";
|
|
3
3
|
import { readFileSync, readdirSync } from "node:fs";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
//#region src/migrations.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as projectDirArgs, c as resolveProject, l as stagingUrl, s as projectUrl } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import { n as openBrowser, r as reportErrors } from "./ui-
|
|
2
|
+
import { n as openBrowser, r as reportErrors } from "./ui-CeupNHNa.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
//#region src/open.ts
|
|
5
5
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as siteConfig, s as projectUrl, u as tryReadCanopyJson } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import { a as unwrap, i as requireTty, l as apiRequest, r as reportErrors } from "./ui-
|
|
2
|
+
import { a as unwrap, i as requireTty, l as apiRequest, r as reportErrors } from "./ui-CeupNHNa.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { join, relative, resolve, sep } from "node:path";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { c as resolveProject, o as projectFlagArgs } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import { d as withContext, l as apiRequest, r as reportErrors, t as formatDateTime } from "./ui-
|
|
2
|
+
import { d as withContext, l as apiRequest, r as reportErrors, t as formatDateTime } from "./ui-CeupNHNa.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
//#region src/schedule.ts
|
|
5
5
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as projectDirArgs, c as resolveProject } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import { a as unwrap, d as withContext, l as apiRequest, r as reportErrors, t as formatDateTime } from "./ui-
|
|
2
|
+
import { a as unwrap, d as withContext, l as apiRequest, r as reportErrors, t as formatDateTime } from "./ui-CeupNHNa.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import * as p from "@clack/prompts";
|
|
5
5
|
//#region src/secrets.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
2
|
-
import { r as reportErrors } from "./ui-
|
|
1
|
+
import { t as version } from "./package-DzU_2IJi.mjs";
|
|
2
|
+
import { r as reportErrors } from "./ui-CeupNHNa.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import { cpSync, existsSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { homedir } from "node:os";
|
|
@@ -581,6 +581,243 @@ function dbRouter(config) {
|
|
|
581
581
|
});
|
|
582
582
|
return db;
|
|
583
583
|
}
|
|
584
|
+
/** Serialized JSON bytes of one message's `data`. */
|
|
585
|
+
const REALTIME_MAX_DATA_BYTES = 4096;
|
|
586
|
+
/** Reserved prefix: topics only the project's backend may publish to. */
|
|
587
|
+
const WORKER_TOPIC_PREFIX = "worker:";
|
|
588
|
+
/** How often an idle stream sends a comment so proxies keep it open. */
|
|
589
|
+
const REALTIME_KEEPALIVE_MS = 25e3;
|
|
590
|
+
/**
|
|
591
|
+
* A stream whose reader is this far behind is closed rather than buffered
|
|
592
|
+
* without bound (a stalled or deliberately slow client). The SDK reconnects
|
|
593
|
+
* and re-syncs, exactly as after any other close.
|
|
594
|
+
*/
|
|
595
|
+
const REALTIME_MAX_BUFFER_BYTES = 256 * 1024;
|
|
596
|
+
/** Every stream closes between these two ages, so auth is re-checked on reconnect. */
|
|
597
|
+
const REALTIME_STREAM_MIN_MS = 13 * 6e4;
|
|
598
|
+
const REALTIME_STREAM_MAX_MS = 15 * 6e4;
|
|
599
|
+
/** 1-64 of [A-Za-z0-9_.-], not dots only (`.`/`..` would be eaten by URL path normalization). */
|
|
600
|
+
const TOPIC_BODY = /^(?!\.+$)[A-Za-z0-9_.-]{1,64}$/;
|
|
601
|
+
function validateTopic(name) {
|
|
602
|
+
const body = name.startsWith("worker:") ? name.slice(7) : name;
|
|
603
|
+
return TOPIC_BODY.test(body);
|
|
604
|
+
}
|
|
605
|
+
const isWorkerTopic = (name) => name.startsWith(WORKER_TOPIC_PREFIX);
|
|
606
|
+
/** A subscription's topics: valid, de-duplicated, 1 to REALTIME_MAX_TOPICS. */
|
|
607
|
+
function parseTopicList(raw) {
|
|
608
|
+
const topics = [...new Set(raw)];
|
|
609
|
+
if (topics.length === 0) return {
|
|
610
|
+
ok: false,
|
|
611
|
+
error: "invalid_topic",
|
|
612
|
+
detail: "subscribe to at least one topic"
|
|
613
|
+
};
|
|
614
|
+
const bad = topics.find((t) => !validateTopic(t));
|
|
615
|
+
if (bad !== void 0) return {
|
|
616
|
+
ok: false,
|
|
617
|
+
error: "invalid_topic",
|
|
618
|
+
detail: topicDetail(bad)
|
|
619
|
+
};
|
|
620
|
+
if (topics.length > 20) return {
|
|
621
|
+
ok: false,
|
|
622
|
+
error: "too_many_topics",
|
|
623
|
+
detail: `one stream carries at most 20 topics`
|
|
624
|
+
};
|
|
625
|
+
return {
|
|
626
|
+
ok: true,
|
|
627
|
+
topics
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
function topicDetail(name) {
|
|
631
|
+
return `"${name.slice(0, 80)}" is not a topic name: use 1-64 letters, digits, "-", "_" or ".", optionally after "${WORKER_TOPIC_PREFIX}"`;
|
|
632
|
+
}
|
|
633
|
+
/** Time-ordered, unguessable enough to be a message id, not a credential. */
|
|
634
|
+
function newMessageId(now = Date.now()) {
|
|
635
|
+
const random = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(8));
|
|
636
|
+
let hex = "";
|
|
637
|
+
for (const byte of random) hex += byte.toString(16).padStart(2, "0");
|
|
638
|
+
return `${now.toString(36).padStart(9, "0")}${hex}`;
|
|
639
|
+
}
|
|
640
|
+
function stampFrom(sender) {
|
|
641
|
+
return sender.kind === "monk" && sender.email ? {
|
|
642
|
+
kind: "monk",
|
|
643
|
+
email: sender.email
|
|
644
|
+
} : { kind: sender.kind };
|
|
645
|
+
}
|
|
646
|
+
function refusalJson(c, refusal) {
|
|
647
|
+
return c.json({
|
|
648
|
+
error: refusal.error,
|
|
649
|
+
detail: refusal.detail
|
|
650
|
+
}, refusal.status);
|
|
651
|
+
}
|
|
652
|
+
function gate(c, ctx) {
|
|
653
|
+
if (!ctx.broker) return c.json({
|
|
654
|
+
error: "not_supported_on_target",
|
|
655
|
+
capability: "realtime",
|
|
656
|
+
detail: "realtime is not available on this Canopy site"
|
|
657
|
+
}, 501);
|
|
658
|
+
if (!ctx.enabled) return c.json({
|
|
659
|
+
error: "realtime_not_enabled",
|
|
660
|
+
detail: "realtime is turned off for this project — a Canopy admin can turn it on for you"
|
|
661
|
+
}, 403);
|
|
662
|
+
}
|
|
663
|
+
const encoder = new TextEncoder();
|
|
664
|
+
const frame = (event, data) => encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
665
|
+
const KEEPALIVE = encoder.encode(": keep-alive\n\n");
|
|
666
|
+
function realtimeRouter(config) {
|
|
667
|
+
const realtime = new Hono();
|
|
668
|
+
const random = config.random ?? Math.random;
|
|
669
|
+
realtime.get("/realtime", async (c) => {
|
|
670
|
+
if (c.req.method !== "GET") return c.json({
|
|
671
|
+
error: "method_not_allowed",
|
|
672
|
+
detail: "subscribe with GET"
|
|
673
|
+
}, 405);
|
|
674
|
+
const ctx = await config.context(c);
|
|
675
|
+
if (ctx instanceof Response) return ctx;
|
|
676
|
+
const refused = gate(c, ctx);
|
|
677
|
+
if (refused) return refused;
|
|
678
|
+
const list = parseTopicList(c.req.queries("topic") ?? []);
|
|
679
|
+
if (!list.ok) return c.json({
|
|
680
|
+
error: list.error,
|
|
681
|
+
detail: list.detail
|
|
682
|
+
}, 400);
|
|
683
|
+
const broker = ctx.broker;
|
|
684
|
+
let controller;
|
|
685
|
+
let handle;
|
|
686
|
+
let ended = false;
|
|
687
|
+
let keepAlive;
|
|
688
|
+
let lifetime;
|
|
689
|
+
const send = (bytes) => {
|
|
690
|
+
if (ended) return;
|
|
691
|
+
try {
|
|
692
|
+
controller.enqueue(bytes);
|
|
693
|
+
} catch {
|
|
694
|
+
end(false);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
if ((controller.desiredSize ?? 0) < 0) {
|
|
698
|
+
end(false);
|
|
699
|
+
try {
|
|
700
|
+
controller.error(/* @__PURE__ */ new Error("realtime stream reader fell behind"));
|
|
701
|
+
} catch {}
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
const end = (closeStream) => {
|
|
705
|
+
if (ended) return;
|
|
706
|
+
ended = true;
|
|
707
|
+
clearInterval(keepAlive);
|
|
708
|
+
clearTimeout(lifetime);
|
|
709
|
+
handle?.close();
|
|
710
|
+
if (closeStream) try {
|
|
711
|
+
controller.close();
|
|
712
|
+
} catch {}
|
|
713
|
+
};
|
|
714
|
+
const stream = new ReadableStream({
|
|
715
|
+
start(ctrl) {
|
|
716
|
+
controller = ctrl;
|
|
717
|
+
},
|
|
718
|
+
cancel() {
|
|
719
|
+
end(false);
|
|
720
|
+
}
|
|
721
|
+
}, new ByteLengthQueuingStrategy({ highWaterMark: REALTIME_MAX_BUFFER_BYTES }));
|
|
722
|
+
const sink = {
|
|
723
|
+
deliver(envelope) {
|
|
724
|
+
const out = ctx.redactEmails && envelope.from.email !== void 0 ? {
|
|
725
|
+
...envelope,
|
|
726
|
+
from: { kind: envelope.from.kind }
|
|
727
|
+
} : envelope;
|
|
728
|
+
send(frame("message", out));
|
|
729
|
+
},
|
|
730
|
+
close() {
|
|
731
|
+
end(true);
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
send(frame("open", { topics: list.topics }));
|
|
735
|
+
const signal = c.req.raw.signal;
|
|
736
|
+
signal?.addEventListener("abort", () => end(false), { once: true });
|
|
737
|
+
let subscribed;
|
|
738
|
+
try {
|
|
739
|
+
subscribed = await broker.subscribe(ctx.scope, list.topics, sink, ctx.owner);
|
|
740
|
+
} catch (error) {
|
|
741
|
+
config.onError?.(c, error);
|
|
742
|
+
ended = true;
|
|
743
|
+
return c.json({
|
|
744
|
+
error: "realtime_unavailable",
|
|
745
|
+
detail: "realtime is briefly unavailable, retry shortly"
|
|
746
|
+
}, 503);
|
|
747
|
+
}
|
|
748
|
+
if ("refused" in subscribed) {
|
|
749
|
+
ended = true;
|
|
750
|
+
return refusalJson(c, subscribed);
|
|
751
|
+
}
|
|
752
|
+
handle = subscribed;
|
|
753
|
+
if (ended || signal?.aborted) {
|
|
754
|
+
end(false);
|
|
755
|
+
handle.close();
|
|
756
|
+
} else {
|
|
757
|
+
keepAlive = setInterval(() => send(KEEPALIVE), REALTIME_KEEPALIVE_MS);
|
|
758
|
+
const span = REALTIME_STREAM_MAX_MS - REALTIME_STREAM_MIN_MS;
|
|
759
|
+
lifetime = setTimeout(() => end(true), REALTIME_STREAM_MIN_MS + Math.floor(random() * span));
|
|
760
|
+
}
|
|
761
|
+
return new Response(stream, {
|
|
762
|
+
status: 200,
|
|
763
|
+
headers: {
|
|
764
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
765
|
+
"cache-control": "private, no-store, no-transform",
|
|
766
|
+
"x-accel-buffering": "no"
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
});
|
|
770
|
+
realtime.post("/realtime/:topic", async (c) => {
|
|
771
|
+
const ctx = await config.context(c);
|
|
772
|
+
if (ctx instanceof Response) return ctx;
|
|
773
|
+
const refused = gate(c, ctx);
|
|
774
|
+
if (refused) return refused;
|
|
775
|
+
const topic = c.req.param("topic");
|
|
776
|
+
if (!validateTopic(topic)) return c.json({
|
|
777
|
+
error: "invalid_topic",
|
|
778
|
+
detail: topicDetail(topic)
|
|
779
|
+
}, 400);
|
|
780
|
+
if (!ctx.isRuntime && isWorkerTopic(topic)) return c.json({
|
|
781
|
+
error: "topic_worker_only",
|
|
782
|
+
detail: `only the project's backend can publish to "${topic}" — pages can subscribe to it, but publish from a worker route instead`
|
|
783
|
+
}, 403);
|
|
784
|
+
const body = await c.req.json().catch(() => void 0);
|
|
785
|
+
if (typeof body !== "object" || body === null || Array.isArray(body) || body.data === void 0) return c.json({
|
|
786
|
+
error: "invalid_message",
|
|
787
|
+
detail: "send a JSON body of the form { \"data\": … }"
|
|
788
|
+
}, 400);
|
|
789
|
+
const data = body.data;
|
|
790
|
+
const bytes = encoder.encode(JSON.stringify(data)).byteLength;
|
|
791
|
+
if (bytes > 4096) return c.json({
|
|
792
|
+
error: "message_too_large",
|
|
793
|
+
detail: `message data is ${bytes} bytes; the limit is ${REALTIME_MAX_DATA_BYTES}. Send an id and load the rest from the database`
|
|
794
|
+
}, 413);
|
|
795
|
+
const envelope = {
|
|
796
|
+
id: newMessageId(),
|
|
797
|
+
topic,
|
|
798
|
+
data,
|
|
799
|
+
from: stampFrom(ctx.sender),
|
|
800
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
801
|
+
};
|
|
802
|
+
let refusal;
|
|
803
|
+
try {
|
|
804
|
+
refusal = await ctx.broker.publish(ctx.scope, envelope, {
|
|
805
|
+
bytes,
|
|
806
|
+
dailyQuota: ctx.dailyQuota,
|
|
807
|
+
dailyBytesQuota: ctx.dailyBytesQuota
|
|
808
|
+
});
|
|
809
|
+
} catch (error) {
|
|
810
|
+
config.onError?.(c, error);
|
|
811
|
+
return c.json({
|
|
812
|
+
error: "realtime_unavailable",
|
|
813
|
+
detail: "realtime is briefly unavailable, retry shortly"
|
|
814
|
+
}, 503);
|
|
815
|
+
}
|
|
816
|
+
if (refusal) return refusalJson(c, refusal);
|
|
817
|
+
return c.json({ id: envelope.id }, 202);
|
|
818
|
+
});
|
|
819
|
+
return realtime;
|
|
820
|
+
}
|
|
584
821
|
//#endregion
|
|
585
822
|
//#region ../data-plane/src/worker-request.ts
|
|
586
823
|
/**
|
|
@@ -652,4 +889,4 @@ function stampWorkerHeaders(headers, ctx) {
|
|
|
652
889
|
return headers;
|
|
653
890
|
}
|
|
654
891
|
//#endregion
|
|
655
|
-
export {
|
|
892
|
+
export { META_COLUMNS as a, dbRouter as i, stampWorkerHeaders as n, blobRouter as o, realtimeRouter as r, redactCanopyCredentials as t };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { c as resolveProject, l as stagingUrl, o as projectFlagArgs, s as projectUrl } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import { a as unwrap, d as withContext, i as requireTty, l as apiRequest, r as reportErrors, t as formatDateTime } from "./ui-
|
|
2
|
+
import { a as unwrap, d as withContext, i as requireTty, l as apiRequest, r as reportErrors, t as formatDateTime } from "./ui-CeupNHNa.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import * as p from "@clack/prompts";
|
|
5
5
|
//#region src/staging.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as projectDirArgs, c as resolveProject, o as projectFlagArgs } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import { l as apiRequest, r as reportErrors, t as formatDateTime } from "./ui-
|
|
2
|
+
import { l as apiRequest, r as reportErrors, t as formatDateTime } from "./ui-CeupNHNa.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
const tokenCommand = defineCommand({
|
|
5
5
|
meta: {
|
|
@@ -38,6 +38,10 @@ const ERROR_COPY = {
|
|
|
38
38
|
why: "the project is paused",
|
|
39
39
|
fix: "Resume it from the dashboard, then retry."
|
|
40
40
|
},
|
|
41
|
+
project_moved: {
|
|
42
|
+
why: "the project was marked as moved to another URL, so its hostnames only redirect",
|
|
43
|
+
fix: "Deploy to the project's new home instead, or undo the move under Settings in the dashboard and retry."
|
|
44
|
+
},
|
|
41
45
|
no_database: {
|
|
42
46
|
why: "the project has no database yet",
|
|
43
47
|
fix: "Deploy the project once (`canopy deploy`) to provision its database, then retry."
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as PACKAGE_NAME } from "./version-check-CcsA1QjI.mjs";
|
|
2
|
-
import { r as reportErrors } from "./ui-
|
|
2
|
+
import { r as reportErrors } from "./ui-CeupNHNa.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -112,7 +112,7 @@ const upgradeCommand = defineCommand({
|
|
|
112
112
|
const spec = `${PACKAGE_NAME}@${args.version ?? "latest"}`;
|
|
113
113
|
const command = pmCommand(pm);
|
|
114
114
|
const cmd = installArgs(pm, spec);
|
|
115
|
-
const { skillInstalled } = await import("./skill-
|
|
115
|
+
const { skillInstalled } = await import("./skill-CQ4a__Ga.mjs");
|
|
116
116
|
const { homedir } = await import("node:os");
|
|
117
117
|
const refreshGlobal = skillInstalled(homedir());
|
|
118
118
|
const refreshRepo = process.cwd() !== homedir() && skillInstalled(process.cwd());
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { c as resolveProject, o as projectFlagArgs } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import { d as withContext, l as apiRequest, r as reportErrors } from "./ui-
|
|
2
|
+
import { d as withContext, l as apiRequest, r as reportErrors } from "./ui-CeupNHNa.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
//#region src/usage.ts
|
|
5
5
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mediamonks/canopy-cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.29",
|
|
4
4
|
"description": "Canopy CLI — create, develop and deploy Canopy projects.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"canopy": "./dist/index.mjs"
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"esbuild": "^0.28.1",
|
|
21
21
|
"hono": "^4.8.0",
|
|
22
22
|
"vite-server": "npm:vite@^7",
|
|
23
|
-
"@mediamonks/canopy": "0.0.
|
|
23
|
+
"@mediamonks/canopy": "0.0.9",
|
|
24
24
|
"@mediamonks/canopy-data-plane": "0.0.4"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
package/dist/dev-BOvMCaML.mjs
DELETED