@mediamonks/canopy-cli 0.4.1 → 0.4.3

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.
@@ -0,0 +1,58 @@
1
+ import { t as readConfig } from "./config-C-z7st1X.mjs";
2
+ //#region src/api.ts
3
+ /**
4
+ * Authenticated requests to the Canopy control plane. Credentials, in order:
5
+ * CANOPY_DEPLOY_TOKEN (CI — a project-scoped deploy token, docs/02-decisions.md
6
+ * D11), then the CLI token from `canopy login` (~/.canopy/config.json).
7
+ */
8
+ var ApiError = class extends Error {
9
+ status;
10
+ code;
11
+ constructor(status, code, detail) {
12
+ super(detail ? `${code}: ${detail}` : code);
13
+ this.status = status;
14
+ this.code = code;
15
+ }
16
+ };
17
+ /** Resolve the bearer token: CI deploy token first (D11), then the login token. */
18
+ function credentials() {
19
+ const config = readConfig();
20
+ const deployToken = process.env["CANOPY_DEPLOY_TOKEN"];
21
+ const token = deployToken || config.token;
22
+ if (!token) throw new Error("Not logged in — run `canopy login` first.");
23
+ return {
24
+ apiUrl: config.apiUrl,
25
+ token,
26
+ isDeployToken: Boolean(deployToken)
27
+ };
28
+ }
29
+ /** Turn a non-2xx response into a thrown error (friendlier copy on 401). */
30
+ async function fail(response, isDeployToken) {
31
+ const payload = await response.json().catch(() => ({}));
32
+ if (response.status === 401) throw new Error(isDeployToken ? "Deploy token rejected — it may have been revoked. Create a new one in the dashboard (Settings → Deploy tokens) or with `canopy token create`." : "Session expired — run `canopy login` again.");
33
+ throw new ApiError(response.status, payload.error ?? `http_${response.status}`, payload.detail);
34
+ }
35
+ async function apiRequest(method, path, body, rawContentType, headers) {
36
+ const { apiUrl, token, isDeployToken } = credentials();
37
+ const isForm = body instanceof FormData;
38
+ const response = await fetch(`${apiUrl}/v1${path}`, {
39
+ method,
40
+ headers: {
41
+ authorization: `Bearer ${token}`,
42
+ ...body === void 0 || isForm ? {} : { "content-type": rawContentType ?? "application/json" },
43
+ ...headers
44
+ },
45
+ body: body === void 0 ? void 0 : isForm ? body : rawContentType !== void 0 ? body : JSON.stringify(body)
46
+ });
47
+ if (!response.ok) return fail(response, isDeployToken);
48
+ return await response.json();
49
+ }
50
+ /** Authenticated GET returning raw bytes (e.g. a deploy zip). */
51
+ async function apiDownload(path) {
52
+ const { apiUrl, token, isDeployToken } = credentials();
53
+ const response = await fetch(`${apiUrl}/v1${path}`, { headers: { authorization: `Bearer ${token}` } });
54
+ if (!response.ok) return fail(response, isDeployToken);
55
+ return Buffer.from(await response.arrayBuffer());
56
+ }
57
+ //#endregion
58
+ export { apiRequest as n, apiDownload as t };
@@ -1,4 +1,4 @@
1
- import { t as apiRequest } from "./api-CyJ-wBZh.mjs";
1
+ import { n as apiRequest } from "./api-BcX_w6Xf.mjs";
2
2
  import { i as resolveProject, n as projectFlagArgs } from "./project-CpiPLNby.mjs";
3
3
  import { i as unwrap, n as reportErrors, r as requireTty } from "./ui-C8VuPzGp.mjs";
4
4
  import { defineCommand } from "citty";
@@ -1,9 +1,9 @@
1
- import { t as apiRequest } from "./api-CyJ-wBZh.mjs";
1
+ import { n as apiRequest, t as apiDownload } from "./api-BcX_w6Xf.mjs";
2
2
  import { a as tryReadCanopyJson, i as resolveProject, n as projectFlagArgs, r as projectUrl, t as projectDirArgs } from "./project-CpiPLNby.mjs";
3
3
  import { i as unwrap, n as reportErrors } from "./ui-C8VuPzGp.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  import { spawn } from "node:child_process";
6
- import { readFileSync, readdirSync, statSync } from "node:fs";
6
+ import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
7
7
  import { join, relative, resolve } from "node:path";
8
8
  import * as p from "@clack/prompts";
9
9
  //#region src/deploy.ts
@@ -97,17 +97,16 @@ const deployCommand = defineCommand({
97
97
  spin.stop(`Live: ${projectUrl(slug)} (v${result.version})`);
98
98
  })
99
99
  });
100
- /** Show the deploy history and ask which version to make live. */
101
- async function pickVersion(deploys, live) {
102
- const fallback = live !== null && live > 1 ? live - 1 : void 0;
100
+ /** Show the deploy history and ask which version to act on. */
101
+ async function pickVersion(deploys, live, message, initial) {
103
102
  return unwrap(await p.select({
104
- message: "Version to make live",
103
+ message,
105
104
  options: deploys.map((deploy) => ({
106
105
  value: deploy.version,
107
106
  label: `v${deploy.version} ${deploy.created_at} (${deploy.source})`,
108
107
  hint: deploy.version === live ? "live" : void 0
109
108
  })),
110
- initialValue: fallback
109
+ initialValue: initial
111
110
  }));
112
111
  }
113
112
  const rollbackCommand = defineCommand({
@@ -137,7 +136,7 @@ const rollbackCommand = defineCommand({
137
136
  const { deploys, live } = await apiRequest("GET", `/projects/${slug}/deploys`);
138
137
  if (deploys.length < 2) throw new Error("Nothing to roll back to — this project has fewer than two deploys.");
139
138
  if (process.stdin.isTTY) {
140
- target = await pickVersion(deploys, live);
139
+ target = await pickVersion(deploys, live, "Version to make live", live !== null && live > 1 ? live - 1 : void 0);
141
140
  if (target === live) throw new Error(`v${target} is already live.`);
142
141
  } else {
143
142
  if (live === null || live < 2) throw new Error("Nothing to roll back to — no deploy exists before the live one.");
@@ -149,5 +148,52 @@ const rollbackCommand = defineCommand({
149
148
  console.log(`Live: ${projectUrl(slug)} (v${result.version}, was v${result.previous ?? "?"})`);
150
149
  })
151
150
  });
151
+ const downloadCommand = defineCommand({
152
+ meta: {
153
+ name: "download",
154
+ description: "Download a deploy's assets as a zip (defaults to the live version)"
155
+ },
156
+ args: {
157
+ version: {
158
+ type: "positional",
159
+ description: "Deploy version to download, e.g. v3 (the live version when omitted)",
160
+ required: false
161
+ },
162
+ ...projectFlagArgs,
163
+ dir: {
164
+ type: "string",
165
+ description: "Project directory (defaults to the current directory)"
166
+ },
167
+ out: {
168
+ type: "string",
169
+ description: "Output file (defaults to <slug>-v<version>.zip)"
170
+ }
171
+ },
172
+ run: reportErrors("download", async ({ args }) => {
173
+ const { slug } = resolveProject(args);
174
+ let target;
175
+ if (args.version !== void 0) {
176
+ if (!/^v?\d+$/.test(args.version)) throw new Error(`Invalid version "${args.version}" — expected a number like 3 or v3.`);
177
+ target = Number(args.version.replace(/^v/, ""));
178
+ } else {
179
+ const { deploys, live } = await apiRequest("GET", `/projects/${slug}/deploys`);
180
+ if (deploys.length === 0) throw new Error("Nothing to download — this project has no deploys yet.");
181
+ if (process.stdin.isTTY) target = await pickVersion(deploys, live, "Version to download", live ?? void 0);
182
+ else if (live !== null) target = live;
183
+ else throw new Error("No live version to download — pass a version, e.g. v1.");
184
+ }
185
+ const out = args.out ?? `${slug}-v${target}.zip`;
186
+ const spin = p.spinner();
187
+ spin.start(`Downloading ${slug} v${target}…`);
188
+ try {
189
+ const zip = await apiDownload(`/projects/${slug}/deploys/${target}/download`);
190
+ writeFileSync(out, zip);
191
+ spin.stop(`Wrote ${out} (${(zip.byteLength / 1024).toFixed(0)} kB).`);
192
+ } catch (err) {
193
+ spin.error("Download failed.");
194
+ throw err;
195
+ }
196
+ })
197
+ });
152
198
  //#endregion
153
- export { deployCommand, rollbackCommand };
199
+ export { deployCommand, downloadCommand, rollbackCommand };
@@ -1,9 +1,11 @@
1
1
  import { t as readConfig } from "./config-C-z7st1X.mjs";
2
- import { t as apiRequest } from "./api-CyJ-wBZh.mjs";
2
+ import { n as apiRequest } from "./api-BcX_w6Xf.mjs";
3
3
  import { i as resolveProject, t as projectDirArgs } from "./project-CpiPLNby.mjs";
4
4
  import { n as reportErrors } from "./ui-C8VuPzGp.mjs";
5
+ import { createRequire } from "node:module";
5
6
  import { defineCommand } from "citty";
6
7
  import { join } from "node:path";
8
+ import { pathToFileURL } from "node:url";
7
9
  //#region src/dev.ts
8
10
  /**
9
11
  * Mirror the gateway's project-host rewrite (apps/gateway/src/index.ts):
@@ -18,9 +20,31 @@ function rewriteApiPath(path, slug) {
18
20
  if (dataPlane) pathname = `/v1/projects/${slug}/${dataPlane[1]}${dataPlane[2] ?? ""}`;
19
21
  return pathname + query;
20
22
  }
23
+ /**
24
+ * Load Vite's `createServer`, preferring the Vite installed in the *project*
25
+ * over the copy bundled with the CLI.
26
+ *
27
+ * Why the project's own Vite: the dev server and the project's
28
+ * vite.config/plugins must run on ONE Vite instance. When they don't — e.g. the
29
+ * CLI serves with its Vite while the project's `vite.config.ts` and
30
+ * `@vitejs/plugin-react` pull in a second, separately-installed Vite (even the
31
+ * same version) — internal module resolution breaks: the server fails to
32
+ * resolve `/@vite/client` and the browser gets the HTML fallback instead of the
33
+ * client runtime. Scaffolds that ship a vite.config (spa/worker) always have
34
+ * their own Vite; the no-build `static` scaffold has none, so we fall back to
35
+ * the CLI's bundled Vite (aliased "vite-server"). Upstream Vite is used rather
36
+ * than the repo's Vite+ fork, whose native binding rides the devDependency-only
37
+ * `vite-plus` package and is absent from a user's global `npm i -g` install.
38
+ */
39
+ async function loadCreateServer(dir) {
40
+ try {
41
+ return (await import(pathToFileURL(createRequire(join(dir, "package.json")).resolve("vite")).href)).createServer;
42
+ } catch {
43
+ return (await import("vite-server")).createServer;
44
+ }
45
+ }
21
46
  async function startVite(flags, serverOverrides) {
22
- const { createServer } = await import("vite");
23
- const server = await createServer({
47
+ const server = await (await loadCreateServer(flags.dir))({
24
48
  root: flags.dir,
25
49
  plugins: [{
26
50
  name: "canopy-platform-api-external",
@@ -55,7 +79,7 @@ async function devRemote(flags, slug) {
55
79
  async function devLocal(flags, slug) {
56
80
  let dump;
57
81
  if (flags.pull) {
58
- const { fetchProjectDump } = await import("./local-951zc9qJ.mjs");
82
+ const { fetchProjectDump } = await import("./local-DSGE-qxT.mjs");
59
83
  console.log(`Pulling a fresh export of ${slug}…`);
60
84
  dump = await fetchProjectDump(slug);
61
85
  }
@@ -69,7 +93,7 @@ async function devLocal(flags, slug) {
69
93
  } catch (err) {
70
94
  console.log(`Note: ai/services will be unavailable — could not reach the platform (${err instanceof Error ? err.message : String(err)}).`);
71
95
  }
72
- const { createSandbox } = await import("./local-951zc9qJ.mjs");
96
+ const { createSandbox } = await import("./local-DSGE-qxT.mjs");
73
97
  const sandbox = await createSandbox({
74
98
  slug,
75
99
  persistDir: join(flags.dir, ".canopy", "local"),
@@ -0,0 +1,2 @@
1
+ import { t as devCommand } from "./dev-CGiJJ5mj.mjs";
2
+ export { devCommand };
package/dist/docs/cli.md CHANGED
@@ -44,6 +44,9 @@ is the overview.
44
44
  - `--build` — run the `canopy.json` build command first.
45
45
  - `canopy rollback [version]` — make an earlier deploy live again, e.g. `v3`
46
46
  (picked from history when omitted). `--dir <path>` to target another directory.
47
+ - `canopy download [version]` — download a deploy's assets as a zip, e.g. `v3`
48
+ (the live version when omitted, or picked from history interactively).
49
+ `--out <file>` to name it.
47
50
  - `canopy open` — open the deployed project in your browser.
48
51
 
49
52
  ## Data
@@ -38,6 +38,21 @@ canopy rollback v3 # make v3 live again
38
38
 
39
39
  Rollback re-points the live version at an existing deploy — it does not rebuild.
40
40
 
41
+ ## Downloading a version
42
+
43
+ Every version's assets can be pulled back down as a zip — the exact files that
44
+ were deployed, for any version in the history:
45
+
46
+ ```bash
47
+ canopy download # the live version (pick from history in a terminal)
48
+ canopy download v3 # a specific version
49
+ canopy download v3 --out release.zip
50
+ ```
51
+
52
+ The same download is available per version from the dashboard (project overview
53
+ → Deploys). Handy for archiving a release or moving a matured project off the
54
+ platform.
55
+
41
56
  ## Seeing the live site
42
57
 
43
58
  ```bash
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as version } from "./package-CA5g4zPx.mjs";
2
+ import { t as version } from "./package-M9p3aYWG.mjs";
3
3
  import { i as runRefresh, r as notifyOfUpdate } from "./version-check-CoWgQs2h.mjs";
4
4
  import { defineCommand, runMain } from "citty";
5
5
  //#region src/index.ts
@@ -44,19 +44,20 @@ const main = defineCommand({
44
44
  login: () => import("./login-Dif9Y32q.mjs").then((m) => m.loginCommand),
45
45
  whoami: () => import("./login-Dif9Y32q.mjs").then((m) => m.whoamiCommand),
46
46
  logout: () => import("./login-Dif9Y32q.mjs").then((m) => m.logoutCommand),
47
- create: () => import("./projects-DbKcG5kx.mjs").then((m) => m.createCommand),
48
- init: () => import("./projects-DbKcG5kx.mjs").then((m) => m.initCommand),
49
- projects: () => import("./projects-DbKcG5kx.mjs").then((m) => m.projectsCommand),
50
- delete: () => import("./projects-DbKcG5kx.mjs").then((m) => m.deleteCommand),
51
- dev: () => import("./dev-D_GlUULk.mjs").then((m) => m.devCommand),
52
- deploy: () => import("./deploy-N88l4Kt3.mjs").then((m) => m.deployCommand),
53
- rollback: () => import("./deploy-N88l4Kt3.mjs").then((m) => m.rollbackCommand),
54
- token: () => import("./token-rTD9jxPZ.mjs").then((m) => m.tokenCommand),
55
- db: () => import("./db-CtUG4vfG.mjs").then((m) => m.dbCommand),
47
+ create: () => import("./projects-oyjC4ZGb.mjs").then((m) => m.createCommand),
48
+ init: () => import("./projects-oyjC4ZGb.mjs").then((m) => m.initCommand),
49
+ projects: () => import("./projects-oyjC4ZGb.mjs").then((m) => m.projectsCommand),
50
+ delete: () => import("./projects-oyjC4ZGb.mjs").then((m) => m.deleteCommand),
51
+ dev: () => import("./dev-fNEcOnTg.mjs").then((m) => m.devCommand),
52
+ deploy: () => import("./deploy-BO_-gK32.mjs").then((m) => m.deployCommand),
53
+ rollback: () => import("./deploy-BO_-gK32.mjs").then((m) => m.rollbackCommand),
54
+ download: () => import("./deploy-BO_-gK32.mjs").then((m) => m.downloadCommand),
55
+ token: () => import("./token-BXc2klNm.mjs").then((m) => m.tokenCommand),
56
+ db: () => import("./db-Bkq-_qkd.mjs").then((m) => m.dbCommand),
56
57
  open: () => import("./open-xLiGUz_W.mjs").then((m) => m.openCommand),
57
58
  docs: () => import("./docs-C3aOAx_A.mjs").then((m) => m.docsCommand),
58
- skill: () => import("./skill-DMs7GNl7.mjs").then((m) => m.skillCommand),
59
- upgrade: () => import("./upgrade-BMTdpOxO.mjs").then((m) => m.upgradeCommand),
59
+ skill: () => import("./skill-BYuy0tS3.mjs").then((m) => m.skillCommand),
60
+ upgrade: () => import("./upgrade-fRFEV1GC.mjs").then((m) => m.upgradeCommand),
60
61
  logs: planned("logs", "Tail worker logs", "2")
61
62
  }
62
63
  });
@@ -1,5 +1,5 @@
1
- import { t as apiRequest } from "./api-CyJ-wBZh.mjs";
2
- import { n as rewriteApiPath } from "./dev-DTGW0lm0.mjs";
1
+ import { n as apiRequest } from "./api-BcX_w6Xf.mjs";
2
+ import { n as rewriteApiPath } from "./dev-CGiJJ5mj.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  import { dirname, join } from "node:path";
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.4.1";
2
+ var version = "0.4.3";
3
3
  //#endregion
4
4
  export { version as t };
@@ -1,4 +1,4 @@
1
- import { t as apiRequest } from "./api-CyJ-wBZh.mjs";
1
+ import { n as apiRequest } from "./api-BcX_w6Xf.mjs";
2
2
  import { a as tryReadCanopyJson, r as projectUrl } from "./project-CpiPLNby.mjs";
3
3
  import { i as unwrap, n as reportErrors, r as requireTty } from "./ui-C8VuPzGp.mjs";
4
4
  import { defineCommand } from "citty";
@@ -1,4 +1,4 @@
1
- import { t as version } from "./package-CA5g4zPx.mjs";
1
+ import { t as version } from "./package-M9p3aYWG.mjs";
2
2
  import { n as reportErrors } from "./ui-C8VuPzGp.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import { cpSync, existsSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
@@ -1,4 +1,4 @@
1
- import { t as apiRequest } from "./api-CyJ-wBZh.mjs";
1
+ import { n as apiRequest } from "./api-BcX_w6Xf.mjs";
2
2
  import { i as resolveProject, n as projectFlagArgs, t as projectDirArgs } from "./project-CpiPLNby.mjs";
3
3
  import { n as reportErrors } from "./ui-C8VuPzGp.mjs";
4
4
  import { defineCommand } from "citty";
@@ -112,11 +112,12 @@ 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-BYuy0tS3.mjs");
116
+ const refreshSkill = skillInstalled(process.cwd());
115
117
  console.log(`Upgrading via ${command}: ${command} ${cmd.join(" ")}`);
116
118
  await run(command, cmd);
117
119
  console.log(`\nDone. Run \`canopy --version\` to confirm.`);
118
- const { skillInstalled } = await import("./skill-DMs7GNl7.mjs");
119
- if (skillInstalled(process.cwd())) {
120
+ if (refreshSkill) {
120
121
  console.log("Refreshing the Canopy agent skill in this repo…");
121
122
  await run("canopy", ["skill", "install"]).catch((err) => {
122
123
  console.warn(`Could not auto-refresh the skill (${err instanceof Error ? err.message : String(err)}). Run \`canopy skill install\` yourself.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mediamonks/canopy-cli",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Canopy CLI — create, develop and deploy Canopy projects.",
5
5
  "bin": {
6
6
  "canopy": "./dist/index.mjs"
@@ -18,9 +18,9 @@
18
18
  "citty": "^0.2.2",
19
19
  "hono": "^4.8.0",
20
20
  "miniflare": "^4.20260714.0",
21
- "vite": "npm:@voidzero-dev/vite-plus-core@0.2.5",
21
+ "vite-server": "npm:vite@^7",
22
22
  "@mediamonks/canopy-data-plane": "0.0.1",
23
- "@mediamonks/canopy": "0.0.1"
23
+ "@mediamonks/canopy": "0.0.2"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/node": "^26.1.1",
@@ -1,40 +0,0 @@
1
- import { t as readConfig } from "./config-C-z7st1X.mjs";
2
- //#region src/api.ts
3
- /**
4
- * Authenticated requests to the Canopy control plane. Credentials, in order:
5
- * CANOPY_DEPLOY_TOKEN (CI — a project-scoped deploy token, docs/02-decisions.md
6
- * D11), then the CLI token from `canopy login` (~/.canopy/config.json).
7
- */
8
- var ApiError = class extends Error {
9
- status;
10
- code;
11
- constructor(status, code, detail) {
12
- super(detail ? `${code}: ${detail}` : code);
13
- this.status = status;
14
- this.code = code;
15
- }
16
- };
17
- async function apiRequest(method, path, body, rawContentType, headers) {
18
- const config = readConfig();
19
- const deployToken = process.env["CANOPY_DEPLOY_TOKEN"];
20
- const token = deployToken || config.token;
21
- if (!token) throw new Error("Not logged in — run `canopy login` first.");
22
- const isForm = body instanceof FormData;
23
- const response = await fetch(`${config.apiUrl}/v1${path}`, {
24
- method,
25
- headers: {
26
- authorization: `Bearer ${token}`,
27
- ...body === void 0 || isForm ? {} : { "content-type": rawContentType ?? "application/json" },
28
- ...headers
29
- },
30
- body: body === void 0 ? void 0 : isForm ? body : rawContentType !== void 0 ? body : JSON.stringify(body)
31
- });
32
- if (!response.ok) {
33
- const payload = await response.json().catch(() => ({}));
34
- if (response.status === 401) throw new Error(deployToken ? "Deploy token rejected — it may have been revoked. Create a new one in the dashboard (Settings → Deploy tokens) or with `canopy token create`." : "Session expired — run `canopy login` again.");
35
- throw new ApiError(response.status, payload.error ?? `http_${response.status}`, payload.detail);
36
- }
37
- return await response.json();
38
- }
39
- //#endregion
40
- export { apiRequest as t };
@@ -1,2 +0,0 @@
1
- import { t as devCommand } from "./dev-DTGW0lm0.mjs";
2
- export { devCommand };