@mediamonks/canopy-cli 0.4.5 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +43 -0
  2. package/dist/{db-PpaU3V-B.mjs → db-BW1ZolWn.mjs} +8 -11
  3. package/dist/{deploy-B064KXtr.mjs → deploy-alC2--pZ.mjs} +92 -12
  4. package/dist/dev-BJ5xEwqW.mjs +2 -0
  5. package/dist/{dev-CuvlJpcO.mjs → dev-CQQhMOoD.mjs} +22 -24
  6. package/dist/docs/ci.md +6 -2
  7. package/dist/docs/cli.md +8 -3
  8. package/dist/docs/data.md +22 -4
  9. package/dist/docs/deploy.md +10 -6
  10. package/dist/docs/dev.md +10 -3
  11. package/dist/docs/infrastructure.md +397 -0
  12. package/dist/docs/static-serving.md +318 -0
  13. package/dist/docs/troubleshooting.md +14 -0
  14. package/dist/{docs-UYzmzpz-.mjs → docs-DvSPpSIR.mjs} +8 -7
  15. package/dist/index.mjs +53 -19
  16. package/dist/local-C5-GWJcx.mjs +826 -0
  17. package/dist/login-Bz-rwlFF.mjs +264 -0
  18. package/dist/{open-CBuZBJlv.mjs → open-DnlYDqrU.mjs} +2 -2
  19. package/dist/{package-Bnu23N0A.mjs → package-Bi5FfVSP.mjs} +1 -1
  20. package/dist/{projects-CK4DNxgZ.mjs → projects-ijfe7Ffx.mjs} +21 -3
  21. package/dist/site-BJjoePQS.mjs +254 -0
  22. package/dist/skill/canopy/SKILL.md +1 -1
  23. package/dist/{skill-h7QlCMhc.mjs → skill-IAAa2EkK.mjs} +2 -2
  24. package/dist/templates/static/AGENTS.md +1 -1
  25. package/dist/templates/static/main.js +1 -1
  26. package/dist/templates/worker/AGENTS.md +11 -9
  27. package/dist/templates/worker/worker/index.ts +27 -4
  28. package/dist/{token-yuwQTQB8.mjs → token-BeF9zczg.mjs} +2 -2
  29. package/dist/{ui-D1DXoDN1.mjs → ui-DThlyHgo.mjs} +28 -36
  30. package/dist/{upgrade-sXoqYLRu.mjs → upgrade-DNxR-zh7.mjs} +3 -3
  31. package/dist/usage-DQpH7A3X.mjs +79 -0
  32. package/package.json +4 -3
  33. package/dist/dev-BYv6BmcI.mjs +0 -2
  34. package/dist/local-B3dyT2fh.mjs +0 -563
  35. package/dist/login-DdNc9OjU.mjs +0 -173
  36. package/dist/project-DKbgJACM.mjs +0 -54
  37. package/dist/{version-check-CoWgQs2h.mjs → version-check-Cuh6HaLX.mjs} +1 -1
@@ -4,9 +4,10 @@
4
4
  * platform (db/blob/ai) through the CANOPY service binding, so budgets and
5
5
  * data isolation still apply.
6
6
  *
7
- * NOTE: per-project Workers are a Canopy Phase 2 capability. `canopy deploy`
8
- * does not upload this Worker yet — the frontend deploys like any SPA, and
9
- * this directory is picked up automatically once the platform supports it.
7
+ * On GCP, `canopy deploy` bundles this file and rolls a per-project Cloud Run
8
+ * service (docs/05-gcp-plan.md G6). Routes under `/api/*` reach this Worker;
9
+ * everything else serves the SPA. Dashboard schedules call `scheduled()`
10
+ * (docs/07-schedules.md).
10
11
  */
11
12
 
12
13
  import { createCanopy } from "@mediamonks/canopy";
@@ -15,6 +16,15 @@ import { Hono } from "hono";
15
16
  interface Env {
16
17
  // Service binding to the platform API (canopy-api), scoped to this project.
17
18
  CANOPY: { fetch: typeof fetch };
19
+ // Dashboard secrets (Settings → Secrets) land as Workers-shaped bindings:
20
+ // MY_API_KEY?: string;
21
+ }
22
+
23
+ interface ScheduledEvent {
24
+ name: string;
25
+ cron: string;
26
+ scheduledTime: number;
27
+ type: "scheduled";
18
28
  }
19
29
 
20
30
  const app = new Hono<{ Bindings: Env }>();
@@ -26,4 +36,17 @@ app.get("/api/hello", async (c) => {
26
36
  return c.json({ message: `Hello from the backend, ${user.name}!` });
27
37
  });
28
38
 
29
- export default app;
39
+ export default {
40
+ fetch: app.fetch.bind(app),
41
+
42
+ // Settings → Schedules: switch on event.name (not the cron string).
43
+ async scheduled(event: ScheduledEvent, _env: Env) {
44
+ switch (event.name) {
45
+ // case "daily-digest":
46
+ // await doWork(_env);
47
+ // break;
48
+ default:
49
+ console.warn("unhandled schedule", event.name);
50
+ }
51
+ },
52
+ };
@@ -1,5 +1,5 @@
1
- import { n as reportErrors, s as apiRequest } from "./ui-D1DXoDN1.mjs";
2
- import { i as resolveProject, n as projectFlagArgs, t as projectDirArgs } from "./project-DKbgJACM.mjs";
1
+ import { a as projectFlagArgs, i as projectDirArgs, s as resolveProject } from "./site-BJjoePQS.mjs";
2
+ import { c as apiRequest, n as reportErrors } from "./ui-DThlyHgo.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  const tokenCommand = defineCommand({
5
5
  meta: {
@@ -1,37 +1,6 @@
1
+ import { n as siteConfig } from "./site-BJjoePQS.mjs";
1
2
  import { spawn } from "node:child_process";
2
- import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
- import { homedir } from "node:os";
4
- import { join } from "node:path";
5
3
  import { cancel, isCancel } from "@clack/prompts";
6
- //#region src/config.ts
7
- /**
8
- * CLI config at ~/.canopy/config.json — platform endpoints and the current
9
- * CLI token. Env overrides: CANOPY_API_URL, CANOPY_OIDC_ISSUER,
10
- * CANOPY_OIDC_CLIENT_ID.
11
- */
12
- const DEFAULT_API_URL = "https://canopy-api.arjan-van-wijk.workers.dev";
13
- const DEFAULT_ISSUER = "https://s4capital.onelogin.com/oidc/2";
14
- const DEFAULT_CLI_CLIENT_ID = "b69755d0-6678-013f-b5ce-09fe44a57aa0180501";
15
- const configDir = join(homedir(), ".canopy");
16
- const configPath = join(configDir, "config.json");
17
- function readConfig() {
18
- let stored = {};
19
- try {
20
- stored = JSON.parse(readFileSync(configPath, "utf8"));
21
- } catch {}
22
- return {
23
- ...stored,
24
- apiUrl: process.env["CANOPY_API_URL"] ?? stored.apiUrl ?? DEFAULT_API_URL,
25
- issuer: process.env["CANOPY_OIDC_ISSUER"] ?? stored.issuer ?? DEFAULT_ISSUER,
26
- cliClientId: process.env["CANOPY_OIDC_CLIENT_ID"] ?? stored.cliClientId ?? DEFAULT_CLI_CLIENT_ID
27
- };
28
- }
29
- function writeConfig(config) {
30
- mkdirSync(configDir, { recursive: true });
31
- writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
32
- chmodSync(configPath, 384);
33
- }
34
- //#endregion
35
4
  //#region src/api.ts
36
5
  /**
37
6
  * Authenticated requests to the Canopy control plane. Credentials, in order:
@@ -81,6 +50,10 @@ const ERROR_COPY = {
81
50
  why: "the AI service is temporarily unavailable",
82
51
  fix: "This is usually transient — wait a moment and try again."
83
52
  },
53
+ not_supported_on_target: {
54
+ why: "this Canopy deployment's cloud target doesn't support that feature",
55
+ fix: "Per-project workers, schedules and secrets are capability-gated per target — remove the unsupported part (e.g. the worker bundle) or run against a target that supports it."
56
+ },
84
57
  deploy_too_large: {
85
58
  why: "the deploy bundle exceeds the 25 MB limit",
86
59
  fix: "Trim large assets from the build output (or move them to blob storage), then redeploy."
@@ -190,7 +163,7 @@ async function withContext(request, context) {
190
163
  }
191
164
  /** Resolve the bearer token: CI deploy token first (D11), then the login token. */
192
165
  function credentials() {
193
- const config = readConfig();
166
+ const config = siteConfig();
194
167
  const deployToken = process.env["CANOPY_DEPLOY_TOKEN"];
195
168
  const token = deployToken || config.token;
196
169
  if (!token) throw new Error("Not logged in — run `canopy login` first.");
@@ -214,7 +187,7 @@ async function fail(response, isDeployToken, method, path) {
214
187
  async function apiRequest(method, path, body, rawContentType, headers) {
215
188
  const { apiUrl, token, isDeployToken } = credentials();
216
189
  const isForm = body instanceof FormData;
217
- const response = await fetch(`${apiUrl}/v1${path}`, {
190
+ const response = await fetch(`${apiUrl}/control/v1${path}`, {
218
191
  method,
219
192
  headers: {
220
193
  authorization: `Bearer ${token}`,
@@ -229,10 +202,29 @@ async function apiRequest(method, path, body, rawContentType, headers) {
229
202
  /** Authenticated GET returning raw bytes (e.g. a deploy zip). */
230
203
  async function apiDownload(path) {
231
204
  const { apiUrl, token, isDeployToken } = credentials();
232
- const response = await fetch(`${apiUrl}/v1${path}`, { headers: { authorization: `Bearer ${token}` } });
205
+ const response = await fetch(`${apiUrl}/control/v1${path}`, { headers: { authorization: `Bearer ${token}` } });
233
206
  if (!response.ok) return fail(response, isDeployToken, "GET", path);
234
207
  return Buffer.from(await response.arrayBuffer());
235
208
  }
209
+ /**
210
+ * Data-plane call (db safety: export/snapshots/restore). The CLI token is
211
+ * unscoped; the target project rides as X-Canopy-Project and the api. gateway
212
+ * re-vouches it (docs/10-cli-sites.md D17).
213
+ */
214
+ async function apiDataRequest(method, path, projectSlug, body, rawContentType) {
215
+ const { apiUrl, token, isDeployToken } = credentials();
216
+ const response = await fetch(`${apiUrl}/data/v1${path}`, {
217
+ method,
218
+ headers: {
219
+ authorization: `Bearer ${token}`,
220
+ "x-canopy-project": projectSlug,
221
+ ...body === void 0 ? {} : { "content-type": rawContentType ?? "application/json" }
222
+ },
223
+ body: body === void 0 ? void 0 : rawContentType !== void 0 ? body : JSON.stringify(body)
224
+ });
225
+ if (!response.ok) return fail(response, isDeployToken, method, path);
226
+ return await response.json();
227
+ }
236
228
  //#endregion
237
229
  //#region src/ui.ts
238
230
  /**
@@ -305,4 +297,4 @@ function openBrowser(url) {
305
297
  }).on("error", () => {}).unref();
306
298
  }
307
299
  //#endregion
308
- export { ApiError as a, withContext as c, unwrap as i, readConfig as l, reportErrors as n, apiDownload as o, requireTty as r, apiRequest as s, openBrowser as t, writeConfig as u };
300
+ export { ApiError as a, apiRequest as c, unwrap as i, withContext as l, reportErrors as n, apiDataRequest as o, requireTty as r, apiDownload as s, openBrowser as t };
@@ -1,5 +1,5 @@
1
- import { t as PACKAGE_NAME } from "./version-check-CoWgQs2h.mjs";
2
- import { n as reportErrors } from "./ui-D1DXoDN1.mjs";
1
+ import { t as PACKAGE_NAME } from "./version-check-Cuh6HaLX.mjs";
2
+ import { n as reportErrors } from "./ui-DThlyHgo.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-h7QlCMhc.mjs");
115
+ const { skillInstalled } = await import("./skill-IAAa2EkK.mjs");
116
116
  const refreshSkill = skillInstalled(process.cwd());
117
117
  console.log(`Upgrading via ${command}: ${command} ${cmd.join(" ")}`);
118
118
  await run(command, cmd);
@@ -0,0 +1,79 @@
1
+ import { a as projectFlagArgs, s as resolveProject } from "./site-BJjoePQS.mjs";
2
+ import { c as apiRequest, l as withContext, n as reportErrors } from "./ui-DThlyHgo.mjs";
3
+ import { defineCommand } from "citty";
4
+ //#region src/usage.ts
5
+ /**
6
+ * canopy usage — print a project's AI spend + storage footprint
7
+ * (same data as the dashboard Usage tab / GET /v1/projects/:slug/usage).
8
+ */
9
+ function formatBytes(bytes) {
10
+ if (bytes < 1024) return `${bytes} B`;
11
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} kB`;
12
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
13
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
14
+ }
15
+ function formatUsd(n) {
16
+ if (n === 0) return "$0";
17
+ if (n < 1e-4) return `$${n.toExponential(1)}`;
18
+ if (n < .01) return `$${n.toFixed(4)}`;
19
+ return `$${n.toFixed(2)}`;
20
+ }
21
+ function formatTokens(n) {
22
+ if (n < 1e3) return String(n);
23
+ if (n < 1e6) return `${(n / 1e3).toFixed(1)}k`;
24
+ return `${(n / 1e6).toFixed(2)}M`;
25
+ }
26
+ function pad(label, width = 12) {
27
+ return label.padEnd(width);
28
+ }
29
+ function renderUsage(slug, usage) {
30
+ const lines = [`${slug} — last ${usage.ai.windowDays ?? 30} days`, ""];
31
+ lines.push("AI");
32
+ if (!usage.ai.available) lines.push(` ${pad("status")}unavailable${usage.ai.reason ? ` (${usage.ai.reason})` : ""}`);
33
+ else {
34
+ lines.push(` ${pad("spend")}${formatUsd(usage.ai.costUsd ?? 0)}`);
35
+ lines.push(` ${pad("requests")}${usage.ai.requests ?? 0}`);
36
+ lines.push(` ${pad("tokens")}${formatTokens(usage.ai.tokensIn ?? 0)} in / ${formatTokens(usage.ai.tokensOut ?? 0)} out`);
37
+ if (usage.ai.byModel?.length) {
38
+ lines.push(" by model");
39
+ for (const row of usage.ai.byModel.slice(0, 8)) lines.push(` ${row.model.padEnd(36)} ${String(row.requests).padStart(4)} ${formatUsd(row.costUsd)}`);
40
+ }
41
+ if (usage.ai.truncated) lines.push(" (truncated — more requests in the window than we summarized)");
42
+ }
43
+ lines.push("");
44
+ lines.push("Storage");
45
+ lines.push(usage.db ? ` ${pad("database")}${formatBytes(usage.db.sizeBytes)} · ${usage.db.tables} tables` : ` ${pad("database")}—`);
46
+ lines.push(usage.blob ? ` ${pad("blobs")}${formatBytes(usage.blob.totalBytes)} · ${usage.blob.objects} objects` : ` ${pad("blobs")}—`);
47
+ const deploys = usage.deploys.count;
48
+ const live = usage.deploys.latestVersion != null ? ` · v${usage.deploys.latestVersion} live` : "";
49
+ lines.push(` ${pad("deploys")}${deploys}${live}`);
50
+ return lines.join("\n");
51
+ }
52
+ const usageCommand = defineCommand({
53
+ meta: {
54
+ name: "usage",
55
+ description: "Show AI spend and storage usage for a project"
56
+ },
57
+ args: {
58
+ ...projectFlagArgs,
59
+ json: {
60
+ type: "boolean",
61
+ description: "Print raw JSON (same shape as the dashboard Usage API)",
62
+ default: false
63
+ }
64
+ },
65
+ run: reportErrors("usage", async ({ args }) => {
66
+ const { slug } = resolveProject(args);
67
+ const { usage } = await withContext(apiRequest("GET", `/projects/${slug}/usage`), { action: `look up usage for "${slug}"` });
68
+ if (args.json) {
69
+ console.log(JSON.stringify({
70
+ slug,
71
+ usage
72
+ }, null, 2));
73
+ return;
74
+ }
75
+ console.log(renderUsage(slug, usage));
76
+ })
77
+ });
78
+ //#endregion
79
+ export { usageCommand };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mediamonks/canopy-cli",
3
- "version": "0.4.5",
3
+ "version": "0.5.0",
4
4
  "description": "Canopy CLI — create, develop and deploy Canopy projects.",
5
5
  "bin": {
6
6
  "canopy": "./dist/index.mjs"
@@ -15,11 +15,12 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "@clack/prompts": "^1.7.0",
18
+ "@electric-sql/pglite": "^0.3.14",
18
19
  "citty": "^0.2.2",
20
+ "esbuild": "^0.28.1",
19
21
  "hono": "^4.8.0",
20
- "miniflare": "^4.20260714.0",
21
22
  "vite-server": "npm:vite@^7",
22
- "@mediamonks/canopy": "0.0.2",
23
+ "@mediamonks/canopy": "0.0.3",
23
24
  "@mediamonks/canopy-data-plane": "0.0.1"
24
25
  },
25
26
  "devDependencies": {
@@ -1,2 +0,0 @@
1
- import { t as devCommand } from "./dev-CuvlJpcO.mjs";
2
- export { devCommand };