@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.
- package/README.md +43 -0
- package/dist/{db-PpaU3V-B.mjs → db-BW1ZolWn.mjs} +8 -11
- package/dist/{deploy-B064KXtr.mjs → deploy-alC2--pZ.mjs} +92 -12
- package/dist/dev-BJ5xEwqW.mjs +2 -0
- package/dist/{dev-CuvlJpcO.mjs → dev-CQQhMOoD.mjs} +22 -24
- package/dist/docs/ci.md +6 -2
- package/dist/docs/cli.md +8 -3
- package/dist/docs/data.md +22 -4
- package/dist/docs/deploy.md +10 -6
- package/dist/docs/dev.md +10 -3
- package/dist/docs/infrastructure.md +397 -0
- package/dist/docs/static-serving.md +318 -0
- package/dist/docs/troubleshooting.md +14 -0
- package/dist/{docs-UYzmzpz-.mjs → docs-DvSPpSIR.mjs} +8 -7
- package/dist/index.mjs +53 -19
- package/dist/local-C5-GWJcx.mjs +826 -0
- package/dist/login-Bz-rwlFF.mjs +264 -0
- package/dist/{open-CBuZBJlv.mjs → open-DnlYDqrU.mjs} +2 -2
- package/dist/{package-Bnu23N0A.mjs → package-Bi5FfVSP.mjs} +1 -1
- package/dist/{projects-CK4DNxgZ.mjs → projects-ijfe7Ffx.mjs} +21 -3
- package/dist/site-BJjoePQS.mjs +254 -0
- package/dist/skill/canopy/SKILL.md +1 -1
- package/dist/{skill-h7QlCMhc.mjs → skill-IAAa2EkK.mjs} +2 -2
- package/dist/templates/static/AGENTS.md +1 -1
- package/dist/templates/static/main.js +1 -1
- package/dist/templates/worker/AGENTS.md +11 -9
- package/dist/templates/worker/worker/index.ts +27 -4
- package/dist/{token-yuwQTQB8.mjs → token-BeF9zczg.mjs} +2 -2
- package/dist/{ui-D1DXoDN1.mjs → ui-DThlyHgo.mjs} +28 -36
- package/dist/{upgrade-sXoqYLRu.mjs → upgrade-DNxR-zh7.mjs} +3 -3
- package/dist/usage-DQpH7A3X.mjs +79 -0
- package/package.json +4 -3
- package/dist/dev-BYv6BmcI.mjs +0 -2
- package/dist/local-B3dyT2fh.mjs +0 -563
- package/dist/login-DdNc9OjU.mjs +0 -173
- package/dist/project-DKbgJACM.mjs +0 -54
- 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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
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 {
|
|
2
|
-
import {
|
|
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 =
|
|
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,
|
|
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-
|
|
2
|
-
import { n as reportErrors } from "./ui-
|
|
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-
|
|
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.
|
|
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.
|
|
23
|
+
"@mediamonks/canopy": "0.0.3",
|
|
23
24
|
"@mediamonks/canopy-data-plane": "0.0.1"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
package/dist/dev-BYv6BmcI.mjs
DELETED