@mediamonks/canopy-cli 0.7.1 → 0.7.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.
@@ -1,6 +1,6 @@
1
1
  import { c as resolveProject, o as projectFlagArgs } from "./site-BUlQR117.mjs";
2
- import { i as unwrap, l as withContext, n as reportErrors, o as apiDataRequest, r as requireTty } from "./ui-D8HxX0Ng.mjs";
3
- import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-I1adj0z2.mjs";
2
+ import { i as unwrap, l as withContext, n as reportErrors, o as apiDataRequest, r as requireTty } from "./ui-CpdtK7mt.mjs";
3
+ import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-DpWHuqkQ.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-BUlQR117.mjs";
2
- import { c as apiRequest, i as unwrap, l as withContext, n as reportErrors, o as apiDataRequest, s as apiDownload } from "./ui-D8HxX0Ng.mjs";
3
- import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-I1adj0z2.mjs";
2
+ import { c as apiRequest, i as unwrap, l as withContext, n as reportErrors, o as apiDataRequest, s as apiDownload } from "./ui-CpdtK7mt.mjs";
3
+ import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-DpWHuqkQ.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,6 +1,6 @@
1
1
  import { a as projectDirArgs, c as resolveProject, i as projectDialect, n as siteConfig, u as tryReadCanopyJson } from "./site-BUlQR117.mjs";
2
- import { a as ApiError, c as apiRequest, l as withContext, n as reportErrors } from "./ui-D8HxX0Ng.mjs";
3
- import { r as readMigrationFiles } from "./migrations-I1adj0z2.mjs";
2
+ import { a as ApiError, c as apiRequest, l as withContext, n as reportErrors } from "./ui-CpdtK7mt.mjs";
3
+ import { r as readMigrationFiles } from "./migrations-DpWHuqkQ.mjs";
4
4
  import { createRequire } from "node:module";
5
5
  import { defineCommand } from "citty";
6
6
  import { join } from "node:path";
@@ -112,7 +112,7 @@ async function devRemote(flags, slug) {
112
112
  async function devLocal(flags, slug) {
113
113
  let dump;
114
114
  if (flags.pull) {
115
- const { fetchProjectDump } = await import("./local-C47tmhl7.mjs");
115
+ const { fetchProjectDump } = await import("./local-BHxSZwUT.mjs");
116
116
  console.log(`Pulling a fresh export of ${slug}${flags.staging ? " [staging]" : ""}…`);
117
117
  dump = await fetchProjectDump(slug, flags.staging ? "staging" : "live");
118
118
  }
@@ -127,7 +127,7 @@ async function devLocal(flags, slug) {
127
127
  const reason = err instanceof ApiError ? err.explain().why : String(err);
128
128
  console.log(`Note: ai/services will be unavailable — could not reach the platform (${reason}).`);
129
129
  }
130
- const { createSandbox } = await import("./local-C47tmhl7.mjs");
130
+ const { createSandbox } = await import("./local-BHxSZwUT.mjs");
131
131
  const sandbox = await createSandbox({
132
132
  slug,
133
133
  persistDir: join(flags.dir, ".canopy", "local"),
@@ -0,0 +1,2 @@
1
+ import { t as devCommand } from "./dev-BbAVZcAR.mjs";
2
+ export { devCommand };
package/dist/docs/cli.md CHANGED
@@ -70,6 +70,12 @@ has a full page per command.
70
70
 
71
71
  See `canopy docs ci` for wiring push-to-main deploys.
72
72
 
73
+ ## Outbound hosts (egress)
74
+
75
+ - `canopy egress list` — hosts the project's worker may `fetch` directly.
76
+ - `canopy egress allow <host>` — allow a hostname (or `"*.sub.wildcard"`).
77
+ - `canopy egress remove <host>` — remove an entry.
78
+
73
79
  ## Maintenance
74
80
 
75
81
  - `canopy upgrade` — update the CLI to the latest version. `--version <v>` pins a
@@ -167,8 +167,9 @@ Rules of the road for the worker:
167
167
  - **No filesystem, no long-lived state.** Persist through `canopy.db` and
168
168
  `canopy.blob`.
169
169
  - **Outbound requests are controlled.** Calls to external APIs pass through a
170
- platform-managed egress allowlist — if your project needs to call an
171
- external host, ask the platform team to allow it.
170
+ per-project egress allowlist — deny-by-default, managed by you. Allow the
171
+ hosts your worker calls with `canopy egress allow <host>` or in dashboard
172
+ Settings → Outbound hosts (see [Outbound requests](https://docs.canopy.website/platform/egress)).
172
173
  - Frontend calls are plain `fetch("/api/…")` — same origin, the platform
173
174
  session cookie rides along automatically.
174
175
 
@@ -59,6 +59,24 @@ The project hit its daily AI budget cap. The cap is enforced before every AI
59
59
  call; it resets the next day, or a maintainer can raise the project's budget in
60
60
  the dashboard.
61
61
 
62
+ ## `egress_denied` (403)
63
+
64
+ An outbound `fetch` from your worker was blocked by the platform's egress
65
+ guard: `host <hostname> is not on the project allowlist`. Outbound requests
66
+ are deny-by-default — only platform infrastructure and the hosts on your
67
+ project's own allowlist get through. Add the host and retry (running workers
68
+ pick the change up without a deploy):
69
+
70
+ ```bash
71
+ canopy egress allow api.example.com
72
+ ```
73
+
74
+ or use dashboard Settings → Outbound hosts. Details and entry rules:
75
+ [Outbound requests](https://docs.canopy.website/platform/egress). Calls made through the SDK /
76
+ `env.CANOPY.fetch` don't count as egress and are unaffected. On the Cloudflare
77
+ target outbound requests are currently unrestricted, so this error only
78
+ appears on GCP.
79
+
62
80
  ## Docs are stale
63
81
 
64
82
  `canopy docs` caches the platform's docs. Force a refresh:
@@ -1,5 +1,7 @@
1
+ import { t as version } from "./package-BjMP21nL.mjs";
1
2
  import { n as siteConfig } from "./site-BUlQR117.mjs";
2
- import { n as reportErrors } from "./ui-D8HxX0Ng.mjs";
3
+ import { i as latestKnownVersion, r as compareSemver } from "./version-check-CcsA1QjI.mjs";
4
+ import { n as reportErrors } from "./ui-CpdtK7mt.mjs";
3
5
  import { defineCommand } from "citty";
4
6
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
5
7
  import { homedir } from "node:os";
@@ -172,6 +174,8 @@ const docsCommand = defineCommand({
172
174
  run: reportErrors("docs", async ({ args }) => {
173
175
  const refresh = args.refresh;
174
176
  const topic = args.topic;
177
+ const latest = latestKnownVersion();
178
+ if (latest && compareSemver(version, latest) < 0) console.error(`Note: these docs describe CLI ${latest}; you have ${version} — commands or flags they mention may not exist here. Run \`canopy upgrade\`.\n`);
175
179
  if (!topic) {
176
180
  const topics = await listTopics(refresh);
177
181
  if (topics.length === 0) throw new Error("No docs available — check your connection, or reinstall the CLI.");
@@ -0,0 +1,88 @@
1
+ import { a as projectDirArgs, c as resolveProject } from "./site-BUlQR117.mjs";
2
+ import { c as apiRequest, n as reportErrors } from "./ui-CpdtK7mt.mjs";
3
+ import { defineCommand } from "citty";
4
+ //#region src/egress.ts
5
+ /**
6
+ * canopy egress — manage the project's outbound-host allowlist
7
+ * (docs/16-egress-allowlist.md). Worker fetches to hosts outside the list are
8
+ * denied (`egress_denied`) on targets that enforce egress control; the list
9
+ * lives on the platform, not in canopy.json, so a repo file can never widen
10
+ * egress silently on deploy.
11
+ */
12
+ function printList({ hosts, denied, enforced }, slug) {
13
+ if (hosts.length === 0) console.log(`No extra egress hosts for ${slug} — the worker can reach platform hosts only.`);
14
+ else for (const host of hosts) console.log(host);
15
+ if (denied.length > 0) {
16
+ console.log(`\nRecently blocked (allow with \`canopy egress allow <host>\`):`);
17
+ for (const d of denied) console.log(` ${d.host} ×${d.count} last ${d.last_seen}`);
18
+ }
19
+ if (!enforced) console.error("(note: this site's target does not enforce egress yet — the list is recorded for when it does)");
20
+ }
21
+ const egressCommand = defineCommand({
22
+ meta: {
23
+ name: "egress",
24
+ description: "Manage which outbound hosts the project's worker may fetch"
25
+ },
26
+ subCommands: {
27
+ list: defineCommand({
28
+ meta: {
29
+ name: "list",
30
+ description: "List the project's allowed outbound hosts"
31
+ },
32
+ args: { ...projectDirArgs },
33
+ run: reportErrors("egress list", async ({ args }) => {
34
+ const { slug } = resolveProject(args);
35
+ printList(await apiRequest("GET", `/projects/${slug}/egress`), slug);
36
+ })
37
+ }),
38
+ allow: defineCommand({
39
+ meta: {
40
+ name: "allow",
41
+ description: "Allow the worker to fetch a host (e.g. api.example.com or *.googleapis.com)"
42
+ },
43
+ args: {
44
+ host: {
45
+ type: "positional",
46
+ description: "Hostname to allow",
47
+ required: true
48
+ },
49
+ ...projectDirArgs
50
+ },
51
+ run: reportErrors("egress allow", async ({ args }) => {
52
+ const { slug } = resolveProject(args);
53
+ const current = await apiRequest("GET", `/projects/${slug}/egress`);
54
+ const updated = await apiRequest("PUT", `/projects/${slug}/egress`, { hosts: [...current.hosts, args.host] });
55
+ console.log(`Allowed ${args.host} for ${slug} — running workers pick it up now.\n`);
56
+ printList(updated, slug);
57
+ })
58
+ }),
59
+ remove: defineCommand({
60
+ meta: {
61
+ name: "remove",
62
+ description: "Remove a host from the allowlist"
63
+ },
64
+ args: {
65
+ host: {
66
+ type: "positional",
67
+ description: "Hostname to remove",
68
+ required: true
69
+ },
70
+ ...projectDirArgs
71
+ },
72
+ run: reportErrors("egress remove", async ({ args }) => {
73
+ const { slug } = resolveProject(args);
74
+ const current = await apiRequest("GET", `/projects/${slug}/egress`);
75
+ const wanted = args.host.trim().toLowerCase();
76
+ if (!current.hosts.includes(wanted)) {
77
+ console.error(`${wanted} is not on the allowlist for ${slug} (see \`canopy egress list\`).`);
78
+ process.exit(1);
79
+ }
80
+ const updated = await apiRequest("PUT", `/projects/${slug}/egress`, { hosts: current.hosts.filter((h) => h !== wanted) });
81
+ console.log(`Removed ${wanted} from ${slug} — running workers stop reaching it now.\n`);
82
+ printList(updated, slug);
83
+ })
84
+ })
85
+ }
86
+ });
87
+ //#endregion
88
+ export { egressCommand };
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { t as version } from "./package-WhBa4V15.mjs";
2
+ import { t as version } from "./package-BjMP21nL.mjs";
3
3
  import { r as siteHeaderLine, t as setInvocationProfile } from "./site-BUlQR117.mjs";
4
- import { i as runRefresh, r as notifyOfUpdate } from "./version-check-Cuh6HaLX.mjs";
4
+ import { a as notifyOfUpdate, o as runRefresh } from "./version-check-CcsA1QjI.mjs";
5
5
  import { defineCommand, runMain } from "citty";
6
6
  //#region src/index.ts
7
7
  /**
@@ -73,26 +73,27 @@ const main = defineCommand({
73
73
  description: "canopy — create and deploy vibecoded projects"
74
74
  },
75
75
  subCommands: {
76
- login: () => import("./login-ByD0hcxu.mjs").then((m) => m.loginCommand),
77
- whoami: () => import("./login-ByD0hcxu.mjs").then((m) => m.whoamiCommand),
78
- logout: () => import("./login-ByD0hcxu.mjs").then((m) => m.logoutCommand),
79
- create: () => import("./projects-CSH7KsPi.mjs").then((m) => m.createCommand),
80
- init: () => import("./projects-CSH7KsPi.mjs").then((m) => m.initCommand),
81
- projects: () => import("./projects-CSH7KsPi.mjs").then((m) => m.projectsCommand),
82
- delete: () => import("./projects-CSH7KsPi.mjs").then((m) => m.deleteCommand),
83
- dev: () => import("./dev-CvtoGFW0.mjs").then((m) => m.devCommand),
84
- deploy: () => import("./deploy-BcczXlSQ.mjs").then((m) => m.deployCommand),
85
- promote: () => import("./deploy-BcczXlSQ.mjs").then((m) => m.promoteCommand),
86
- staging: () => import("./staging-9bvgl2B5.mjs").then((m) => m.stagingCommand),
87
- rollback: () => import("./deploy-BcczXlSQ.mjs").then((m) => m.rollbackCommand),
88
- download: () => import("./deploy-BcczXlSQ.mjs").then((m) => m.downloadCommand),
89
- token: () => import("./token-nieJV1w7.mjs").then((m) => m.tokenCommand),
90
- db: () => import("./db-BkUmbGgx.mjs").then((m) => m.dbCommand),
91
- usage: () => import("./usage-qqPtkDtT.mjs").then((m) => m.usageCommand),
92
- open: () => import("./open-DD9oa-Ho.mjs").then((m) => m.openCommand),
93
- docs: () => import("./docs-2cUEug69.mjs").then((m) => m.docsCommand),
94
- skill: () => import("./skill-Di9GQYYx.mjs").then((m) => m.skillCommand),
95
- upgrade: () => import("./upgrade-CZwo05Tz.mjs").then((m) => m.upgradeCommand),
76
+ login: () => import("./login-C1x1l3qg.mjs").then((m) => m.loginCommand),
77
+ whoami: () => import("./login-C1x1l3qg.mjs").then((m) => m.whoamiCommand),
78
+ logout: () => import("./login-C1x1l3qg.mjs").then((m) => m.logoutCommand),
79
+ create: () => import("./projects-D2fDeWNU.mjs").then((m) => m.createCommand),
80
+ init: () => import("./projects-D2fDeWNU.mjs").then((m) => m.initCommand),
81
+ projects: () => import("./projects-D2fDeWNU.mjs").then((m) => m.projectsCommand),
82
+ delete: () => import("./projects-D2fDeWNU.mjs").then((m) => m.deleteCommand),
83
+ dev: () => import("./dev-D6Sy15Wv.mjs").then((m) => m.devCommand),
84
+ deploy: () => import("./deploy-ClioT9ZC.mjs").then((m) => m.deployCommand),
85
+ promote: () => import("./deploy-ClioT9ZC.mjs").then((m) => m.promoteCommand),
86
+ staging: () => import("./staging-95xeXyZN.mjs").then((m) => m.stagingCommand),
87
+ rollback: () => import("./deploy-ClioT9ZC.mjs").then((m) => m.rollbackCommand),
88
+ download: () => import("./deploy-ClioT9ZC.mjs").then((m) => m.downloadCommand),
89
+ token: () => import("./token-BjlqT2Xj.mjs").then((m) => m.tokenCommand),
90
+ egress: () => import("./egress-BkkG9Jzs.mjs").then((m) => m.egressCommand),
91
+ db: () => import("./db-D3RKaLVU.mjs").then((m) => m.dbCommand),
92
+ usage: () => import("./usage-wpew0a2M.mjs").then((m) => m.usageCommand),
93
+ open: () => import("./open-CLSh6oez.mjs").then((m) => m.openCommand),
94
+ docs: () => import("./docs-BwtpnE8W.mjs").then((m) => m.docsCommand),
95
+ skill: () => import("./skill-aTr-RQil.mjs").then((m) => m.skillCommand),
96
+ upgrade: () => import("./upgrade-Dl6DiMhL.mjs").then((m) => m.upgradeCommand),
96
97
  logs: planned("logs", "Tail worker logs", "2")
97
98
  }
98
99
  });
@@ -1,5 +1,5 @@
1
- import { o as apiDataRequest } from "./ui-D8HxX0Ng.mjs";
2
- import { n as rewriteApiPath } from "./dev-DvyKHe2d.mjs";
1
+ import { o as apiDataRequest } from "./ui-CpdtK7mt.mjs";
2
+ import { n as rewriteApiPath } from "./dev-BbAVZcAR.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
5
5
  import { dirname, join, relative, sep } from "node:path";
@@ -1,5 +1,5 @@
1
1
  import { _ as getBakedProfile, d as clearAllProfileTokens, f as listBakedProfiles, g as writeConfigFile, h as upsertProfileAuth, m as setActiveProfile, n as siteConfig, p as readConfigFile, v as isProfileId } from "./site-BUlQR117.mjs";
2
- import { n as reportErrors, t as openBrowser } from "./ui-D8HxX0Ng.mjs";
2
+ import { n as reportErrors, t as openBrowser } from "./ui-CpdtK7mt.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import * as p from "@clack/prompts";
5
5
  import { createHash, randomBytes } from "node:crypto";
@@ -1,5 +1,5 @@
1
1
  import { u as tryReadCanopyJson } from "./site-BUlQR117.mjs";
2
- import { o as apiDataRequest } from "./ui-D8HxX0Ng.mjs";
2
+ import { o as apiDataRequest } from "./ui-CpdtK7mt.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-BUlQR117.mjs";
2
- import { n as reportErrors, t as openBrowser } from "./ui-D8HxX0Ng.mjs";
2
+ import { n as reportErrors, t as openBrowser } from "./ui-CpdtK7mt.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  //#region src/open.ts
5
5
  /**
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.7.1";
2
+ var version = "0.7.3";
3
3
  //#endregion
4
4
  export { version as t };
@@ -1,5 +1,5 @@
1
1
  import { n as siteConfig, s as projectUrl, u as tryReadCanopyJson } from "./site-BUlQR117.mjs";
2
- import { c as apiRequest, i as unwrap, n as reportErrors, r as requireTty } from "./ui-D8HxX0Ng.mjs";
2
+ import { c as apiRequest, i as unwrap, n as reportErrors, r as requireTty } from "./ui-CpdtK7mt.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
- import { t as version } from "./package-WhBa4V15.mjs";
2
- import { n as reportErrors } from "./ui-D8HxX0Ng.mjs";
1
+ import { t as version } from "./package-BjMP21nL.mjs";
2
+ import { n as reportErrors } from "./ui-CpdtK7mt.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import { cpSync, existsSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
5
5
  import { join, relative } from "node:path";
@@ -1,5 +1,5 @@
1
1
  import { c as resolveProject, l as stagingUrl, o as projectFlagArgs, s as projectUrl } from "./site-BUlQR117.mjs";
2
- import { c as apiRequest, i as unwrap, l as withContext, n as reportErrors, r as requireTty } from "./ui-D8HxX0Ng.mjs";
2
+ import { c as apiRequest, i as unwrap, l as withContext, n as reportErrors, r as requireTty } from "./ui-CpdtK7mt.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-BUlQR117.mjs";
2
- import { c as apiRequest, n as reportErrors } from "./ui-D8HxX0Ng.mjs";
2
+ import { c as apiRequest, n as reportErrors } from "./ui-CpdtK7mt.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  const tokenCommand = defineCommand({
5
5
  meta: {
@@ -93,6 +93,18 @@ const ERROR_COPY = {
93
93
  version_not_found: {
94
94
  why: "no deploy with that version exists for this project",
95
95
  fix: "Run `canopy deploys` to list versions you can roll back to."
96
+ },
97
+ invalid_host: {
98
+ why: "that entry is not a plain hostname",
99
+ fix: "Use a bare hostname (api.example.com) or a subdomain wildcard (*.googleapis.com) — no IPs, ports, schemes or paths."
100
+ },
101
+ too_many_hosts: {
102
+ why: "the allowlist is at its 32-entry limit",
103
+ fix: "Remove hosts you no longer need (`canopy egress list` / `canopy egress remove`), or cover several subdomains with one *. wildcard."
104
+ },
105
+ worker_egress_sync_failed: {
106
+ why: "the list was saved but redeploying the running worker with it failed",
107
+ fix: "Retry, or redeploy the project (`canopy deploy`) to apply the new allowlist."
96
108
  }
97
109
  };
98
110
  /** Generic explanation for a status code when we have no per-code copy. */
@@ -274,10 +286,46 @@ function formatCliError(name, err) {
274
286
  }
275
287
  return `canopy ${name}: ${err instanceof Error ? err.message : String(err)}`;
276
288
  }
289
+ const kebab = (s) => s.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
290
+ const camel = (s) => s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
291
+ /**
292
+ * Reject flags a command does not declare. citty parses with strict: false, so
293
+ * a misspelled or not-yet-released flag would otherwise be silently dropped —
294
+ * on `deploy --staging` (pre-0.7) that meant deploying to the wrong channel.
295
+ */
296
+ function assertKnownFlags(name, rawArgs, argDefs) {
297
+ const known = /* @__PURE__ */ new Set([
298
+ "help",
299
+ "h",
300
+ "version",
301
+ "profile"
302
+ ]);
303
+ for (const [key, def] of Object.entries(argDefs)) {
304
+ if (def.type === "positional") continue;
305
+ known.add(key);
306
+ known.add(kebab(key));
307
+ known.add(camel(key));
308
+ const aliases = typeof def.alias === "string" ? [def.alias] : def.alias ?? [];
309
+ for (const a of aliases) known.add(a);
310
+ if (def.type === "boolean") {
311
+ known.add(`no-${key}`);
312
+ known.add(`no-${kebab(key)}`);
313
+ }
314
+ }
315
+ for (const token of rawArgs) {
316
+ if (token === "--") break;
317
+ if (!token.startsWith("-") || token === "-") continue;
318
+ const body = token.replace(/^--?/, "").split("=")[0] ?? "";
319
+ if (!body || /^\d/.test(body)) continue;
320
+ if (!known.has(body)) throw new Error(`Unknown flag --${body}. Run \`canopy ${name} --help\` to see supported flags — if the docs mention it, your CLI may be outdated (\`canopy upgrade\`).`);
321
+ }
322
+ }
277
323
  /** Wrap a command action so failures print an actionable report and exit 1. */
278
324
  function reportErrors(name, action) {
279
325
  return async (...args) => {
280
326
  try {
327
+ const ctx = args[0];
328
+ if (ctx?.rawArgs && ctx.cmd?.args) assertKnownFlags(name, ctx.rawArgs, ctx.cmd.args);
281
329
  await action(...args);
282
330
  } catch (err) {
283
331
  console.error(formatCliError(name, err));
@@ -1,5 +1,5 @@
1
- import { t as PACKAGE_NAME } from "./version-check-Cuh6HaLX.mjs";
2
- import { n as reportErrors } from "./ui-D8HxX0Ng.mjs";
1
+ import { t as PACKAGE_NAME } from "./version-check-CcsA1QjI.mjs";
2
+ import { n as reportErrors } from "./ui-CpdtK7mt.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-Di9GQYYx.mjs");
115
+ const { skillInstalled } = await import("./skill-aTr-RQil.mjs");
116
116
  const refreshSkill = skillInstalled(process.cwd());
117
117
  console.log(`Upgrading via ${command}: ${command} ${cmd.join(" ")}`);
118
118
  await run(command, cmd);
@@ -1,5 +1,5 @@
1
1
  import { c as resolveProject, o as projectFlagArgs } from "./site-BUlQR117.mjs";
2
- import { c as apiRequest, l as withContext, n as reportErrors } from "./ui-D8HxX0Ng.mjs";
2
+ import { c as apiRequest, l as withContext, n as reportErrors } from "./ui-CpdtK7mt.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  //#region src/usage.ts
5
5
  /**
@@ -72,6 +72,10 @@ function writeCache(cache) {
72
72
  function disabled() {
73
73
  return process.env["CANOPY_NO_UPDATE_CHECK"] === "1" || process.env["CI"] !== void 0 || !process.stderr.isTTY;
74
74
  }
75
+ /** Latest version seen on the registry (from cache), if any. */
76
+ function latestKnownVersion() {
77
+ return readCache()?.latest;
78
+ }
75
79
  /**
76
80
  * Print an update hint from cache (if we're behind), then kick off a detached
77
81
  * background refresh when the cache is stale. Never throws, never blocks.
@@ -125,4 +129,4 @@ async function runRefresh() {
125
129
  writeCache(cache);
126
130
  }
127
131
  //#endregion
128
- export { runRefresh as i, REFRESH_ENV as n, notifyOfUpdate as r, PACKAGE_NAME as t };
132
+ export { notifyOfUpdate as a, latestKnownVersion as i, REFRESH_ENV as n, runRefresh as o, compareSemver as r, PACKAGE_NAME as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mediamonks/canopy-cli",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "Canopy CLI — create, develop and deploy Canopy projects.",
5
5
  "bin": {
6
6
  "canopy": "./dist/index.mjs"
@@ -1,2 +0,0 @@
1
- import { t as devCommand } from "./dev-DvyKHe2d.mjs";
2
- export { devCommand };