@mediamonks/canopy-cli 0.7.15 → 0.7.17

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 (30) hide show
  1. package/dist/{db-DKnJQPm5.mjs → db-DUVadyyv.mjs} +2 -2
  2. package/dist/{deploy-CnxJ4TE6.mjs → deploy-DLehZzUr.mjs} +3 -3
  3. package/dist/{dev-CyJT6EfQ.mjs → dev-Bvqpc0zN.mjs} +5 -5
  4. package/dist/dev-DAyB0TRG.mjs +2 -0
  5. package/dist/docs/cli-docs.md +10 -0
  6. package/dist/docs/cli-skill.md +16 -9
  7. package/dist/docs/cli.md +2 -1
  8. package/dist/docs/index.json +1 -1
  9. package/dist/docs/porting-nextjs.md +25 -0
  10. package/dist/{docs-BlzdYmou.mjs → docs-DOopUT5k.mjs} +93 -3
  11. package/dist/{egress-PJp0SZy2.mjs → egress-D-Y0vANf.mjs} +3 -3
  12. package/dist/{feedback-h_j6DBBZ.mjs → feedback-Dmrn3SW3.mjs} +4 -4
  13. package/dist/index.mjs +25 -25
  14. package/dist/{local-BQxsp-ZQ.mjs → local-zKGeU9o5.mjs} +2 -2
  15. package/dist/{login-BDTO3nVw.mjs → login-CqM5bZNZ.mjs} +1 -1
  16. package/dist/{logs-C53y2eAS.mjs → logs-DuQmfRoW.mjs} +1 -1
  17. package/dist/{migrations-C6EcKD6a.mjs → migrations-CuLRUDsH.mjs} +1 -1
  18. package/dist/{open-C5x3tX6I.mjs → open-BXP6rJ7R.mjs} +1 -1
  19. package/dist/{package-CF0dX7UA.mjs → package-X1HbEiNB.mjs} +1 -1
  20. package/dist/{projects-MMbv7MmK.mjs → projects-C6tqDIV_.mjs} +1 -1
  21. package/dist/{schedule-Cq9K0rnE.mjs → schedule-lZA3TPTm.mjs} +2 -2
  22. package/dist/skill/canopy/SKILL.md +30 -6
  23. package/dist/{skill-DaaxgqEk.mjs → skill-Beg6Wyik.mjs} +26 -15
  24. package/dist/{staging-D8G4amjO.mjs → staging-CL0Q2v6t.mjs} +2 -2
  25. package/dist/{token-BUxgRJVA.mjs → token-HAniLS_l.mjs} +2 -2
  26. package/dist/{ui-B1Px5y7q.mjs → ui-BUBYpqoC.mjs} +13 -1
  27. package/dist/{upgrade-DG-7xwOs.mjs → upgrade-D297Aze4.mjs} +18 -7
  28. package/dist/{usage-DjNngyvy.mjs → usage-XCARqs3z.mjs} +1 -1
  29. package/package.json +3 -3
  30. package/dist/dev-BqLvttKc.mjs +0 -2
@@ -1,6 +1,6 @@
1
1
  import { c as resolveProject, o as projectFlagArgs } from "./site-BUlQR117.mjs";
2
- import { i as unwrap, n as reportErrors, o as apiDataRequest, r as requireTty, u as withContext } from "./ui-B1Px5y7q.mjs";
3
- import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-C6EcKD6a.mjs";
2
+ import { a as unwrap, d as withContext, i as requireTty, r as reportErrors, s as apiDataRequest } from "./ui-BUBYpqoC.mjs";
3
+ import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-CuLRUDsH.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, n as reportErrors, o as apiDataRequest, s as apiDownload, u as withContext } from "./ui-B1Px5y7q.mjs";
3
- import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-C6EcKD6a.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-BUBYpqoC.mjs";
3
+ import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-CuLRUDsH.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";
@@ -290,7 +290,7 @@ async function pickVersion(deploys, live, message, initial) {
290
290
  message,
291
291
  options: deploys.map((deploy) => ({
292
292
  value: deploy.version,
293
- label: `v${deploy.version} ${deploy.created_at} (${deploy.source})`,
293
+ label: `v${deploy.version} ${formatDateTime(deploy.created_at)} (${deploy.source})`,
294
294
  hint: deploy.version === live ? "live" : void 0
295
295
  })),
296
296
  initialValue: initial
@@ -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, n as reportErrors, u as withContext } from "./ui-B1Px5y7q.mjs";
3
- import { r as readMigrationFiles } from "./migrations-C6EcKD6a.mjs";
2
+ import { d as withContext, l as apiRequest, o as ApiError, r as reportErrors, t as formatDateTime } from "./ui-BUBYpqoC.mjs";
3
+ import { r as readMigrationFiles } from "./migrations-CuLRUDsH.mjs";
4
4
  import { createRequire } from "node:module";
5
5
  import { defineCommand } from "citty";
6
6
  import { existsSync } from "node:fs";
@@ -160,12 +160,12 @@ async function devRemote(flags, slug) {
160
160
  for (const note of worker?.notes ?? []) console.log(note);
161
161
  console.log("");
162
162
  server.printUrls();
163
- console.log(`\n Dev token expires ${expires_at} — restart canopy dev after that.`);
163
+ console.log(`\n Dev token expires ${formatDateTime(expires_at)} — restart canopy dev after that.`);
164
164
  }
165
165
  async function devLocal(flags, slug) {
166
166
  let dump;
167
167
  if (flags.pull) {
168
- const { fetchProjectDump } = await import("./local-BQxsp-ZQ.mjs");
168
+ const { fetchProjectDump } = await import("./local-zKGeU9o5.mjs");
169
169
  console.log(`Pulling a fresh export of ${slug}${flags.staging ? " [staging]" : ""}…`);
170
170
  dump = await fetchProjectDump(slug, flags.staging ? "staging" : "live");
171
171
  }
@@ -180,7 +180,7 @@ async function devLocal(flags, slug) {
180
180
  const reason = err instanceof ApiError ? err.explain().why : String(err);
181
181
  console.log(`Note: ai/services will be unavailable — could not reach the platform (${reason}).`);
182
182
  }
183
- const { createSandbox } = await import("./local-BQxsp-ZQ.mjs");
183
+ const { createSandbox } = await import("./local-zKGeU9o5.mjs");
184
184
  const sandbox = await createSandbox({
185
185
  slug,
186
186
  persistDir: join(flags.dir, ".canopy", "local"),
@@ -0,0 +1,2 @@
1
+ import { t as devCommand } from "./dev-Bvqpc0zN.mjs";
2
+ export { devCommand };
@@ -10,12 +10,14 @@ description: Read the Canopy docs from the terminal — live from the platform,
10
10
  ```bash
11
11
  canopy docs # list all topics
12
12
  canopy docs <topic> # print one topic
13
+ canopy docs search <terms…> # keyword search across all docs
13
14
  canopy docs <topic> --refresh
14
15
  ```
15
16
 
16
17
  ## Arguments & flags
17
18
 
18
19
  - `[topic]` — positional: the topic slug to print. Omit it to list every topic with a one-line summary, grouped by section. Guides keep bare slugs (`deploy`, `staging`); other sections prefix theirs (`sdk-database`, `cli-deploy`, `platform-schedules`).
20
+ - `search <terms…>` — search every doc for the given terms instead of printing one. Case-insensitive; a doc matches only when every term appears somewhere in its title, summary, or body. Results come back best-first (title and summary hits outrank body mentions), each with the matching lines, capped at ten.
19
21
  - `--refresh` — re-fetch from the platform, bypassing the local cache. Default `false`.
20
22
 
21
23
  ## How it works
@@ -30,6 +32,8 @@ That last step is why `canopy docs` works with no network and no login: a fresh
30
32
 
31
33
  Asking for an unknown topic errors with the list of available slugs. Everything prints as plain Markdown to stdout, so it pipes cleanly into a pager or an agent's context.
32
34
 
35
+ Search runs entirely on the local corpus — the cache plus the bundled snapshot — so it's instant and works offline. It's a locator, not a reader: find the topic, then print it with `canopy docs <topic>` (which fetches fresh content when it can).
36
+
33
37
  ## Examples
34
38
 
35
39
  ```bash
@@ -38,6 +42,12 @@ canopy docs
38
42
 
39
43
  Lists every topic with its summary — the starting point when you're not sure which guide covers what you're doing.
40
44
 
45
+ ```bash
46
+ canopy docs search egress allowlist
47
+ ```
48
+
49
+ Finds the docs that mention both terms — useful when the summaries don't tell you which doc covers a detail, like a specific error code or environment variable.
50
+
41
51
  ```bash
42
52
  canopy docs deploy --refresh
43
53
  ```
@@ -1,32 +1,33 @@
1
1
  ---
2
2
  title: canopy skill
3
- description: Install the Canopy agent skill into a repo so coding agents know how to drive the platform.
3
+ description: Install the Canopy agent skill so coding agents know how to drive the platform — globally by default, or into a repo.
4
4
  ---
5
5
 
6
- `canopy skill install` writes the Canopy agent skill into the current repository, teaching AI coding agents (Claude Code and others) how to use the platform: checking install and login state, creating vs. initializing projects, choosing templates, reading `canopy docs` before guessing, and treating `db restore`, `delete`, and deploys with appropriate care.
6
+ `canopy skill install` writes the Canopy agent skill, teaching AI coding agents (Claude Code and others) how to use the platform: checking install and login state, creating vs. initializing projects, choosing templates, working feedback reports, reading `canopy docs` before guessing, and treating `db restore`, `delete`, and deploys with appropriate care.
7
7
 
8
8
  ## Usage
9
9
 
10
10
  ```bash
11
- canopy skill install
11
+ canopy skill install # global — applies in every repo (default)
12
+ canopy skill install --repo # into the current repo instead
12
13
  ```
13
14
 
14
15
  `install` is currently the only subcommand.
15
16
 
16
17
  ## Arguments & flags
17
18
 
18
- None — it always installs into the repository at the current working directory.
19
+ - `--repo` — install into the repository at the current working directory instead of globally. Use this when you want the skill committed with a project so teammates and CI agents get it with the repo.
19
20
 
20
21
  ## How it works
21
22
 
22
- The skill files ship inside the CLI package, so installing needs no network. Two paths are written:
23
+ The skill files ship inside the CLI package, so installing needs no network. Two paths are written — under your home directory by default, or under the repo with `--repo`:
23
24
 
24
25
  - `.agents/skills/canopy/` — the source of truth, in the tool-neutral `.agents/` tree so any agent tooling can read it (no vendor lock-in).
25
- - `.claude/skills/canopy` — a **symlink** to the `.agents/` copy, so Claude Code picks the skill up. Where symlinks aren't available (e.g. Windows without developer mode), the CLI falls back to a real copy and says so.
26
+ - `.claude/skills/canopy` — a **symlink** to the `.agents/` copy, so Claude Code picks the skill up (the global install lands in `~/.claude/skills/`, Claude Code's user scope, so it applies in every repo). Where symlinks aren't available (e.g. Windows without developer mode), the CLI falls back to a real copy and says so.
26
27
 
27
- Installation is a clean replace: any existing skill directory is removed first, and the installed copy is stamped with the CLI version that wrote it. That stamp is how the skill stays fresh — [`canopy upgrade`](https://docs.canopy.website/cli/upgrade) detects an installed skill in the repo and re-runs `canopy skill install` with the new CLI, so the skill always matches the CLI version in use. If that auto-refresh ever fails, re-run `canopy skill install` yourself.
28
+ Installation is a clean replace: any existing skill directory is removed first, and the installed copy is stamped with the CLI version that wrote it. That stamp is how the skill stays fresh — [`canopy upgrade`](https://docs.canopy.website/cli/upgrade) re-runs `canopy skill install` with the new CLI for the global copy and for a repo copy in the directory you upgrade from, so no stale install shadows a fresh one. If that auto-refresh ever fails, re-run `canopy skill install` yourself.
28
29
 
29
- Commit the `.agents/` directory (and the `.claude/` link) so teammates and CI agents get the skill with the repo.
30
+ With `--repo`, commit the `.agents/` directory (and the `.claude/` link) so teammates and CI agents get the skill with the repo.
30
31
 
31
32
  ## Examples
32
33
 
@@ -34,6 +35,12 @@ Commit the `.agents/` directory (and the `.claude/` link) so teammates and CI ag
34
35
  canopy skill install
35
36
  ```
36
37
 
37
- From a repo root: installs the skill, prints where the files went and whether `.claude/skills/canopy` is a symlink or a copy, and suggests trying `canopy docs`.
38
+ Installs the skill globally, prints where the files went and whether `.claude/skills/canopy` is a symlink or a copy, and suggests trying `canopy docs`.
39
+
40
+ ```bash
41
+ canopy skill install --repo
42
+ ```
43
+
44
+ From a repo root: same, but into this repository — commit the result.
38
45
 
39
46
  See also [canopy docs](https://docs.canopy.website/cli/docs) — the always-current documentation the skill points agents at — and [canopy upgrade](https://docs.canopy.website/cli/upgrade).
package/dist/docs/cli.md CHANGED
@@ -83,4 +83,5 @@ See `canopy docs ci` for wiring push-to-main deploys.
83
83
  Upgrading also refreshes the installed Canopy agent skill.
84
84
  - `canopy docs [topic]` — list docs, or print one. `--refresh` re-fetches from
85
85
  the platform.
86
- - `canopy skill install` — (re)install the Canopy agent skill into this repo.
86
+ - `canopy skill install` — (re)install the Canopy agent skill, globally by
87
+ default; `--repo` installs into the current repo instead.
@@ -274,7 +274,7 @@
274
274
  {
275
275
  "slug": "cli-skill",
276
276
  "title": "canopy skill",
277
- "summary": "Install the Canopy agent skill into a repo so coding agents know how to drive the platform."
277
+ "summary": "Install the Canopy agent skill so coding agents know how to drive the platform — globally by default, or into a repo."
278
278
  },
279
279
  {
280
280
  "slug": "cli-upgrade",
@@ -173,6 +173,31 @@ Rules of the road for the worker:
173
173
  - Frontend calls are plain `fetch("/api/…")` — same origin, the platform
174
174
  session cookie rides along automatically.
175
175
 
176
+ ### Fetching your own API from the worker
177
+
178
+ Ported server code often calls the app's own routes with an absolute URL —
179
+ `fetch(BASE_URL + "/api/…")` — because server-side fetch has no origin to
180
+ resolve against. On Vercel that request loops through their edge; on Canopy a
181
+ worker's fetch is a real outbound request, so it would normally hit the
182
+ egress allowlist, and even an allowed hop would arrive without your session.
183
+
184
+ The platform recognizes this shape and short-circuits it: a fetch to the
185
+ project's **own hostname** (or to `localhost` on any port — the common
186
+ `BASE_URL` fallback) with a path under `/api/` re-enters your worker directly,
187
+ in-process. No network hop, no allowlist entry, and `env.USER` is the same
188
+ caller who made the original request. Two things to know:
189
+
190
+ - The loopback covers `/api/*` only. A self-fetch to any other path fails
191
+ like any other non-allowlisted host.
192
+ - Routes fetching each other in a cycle are cut off after 8 hops with a
193
+ `self_fetch_loop` error, instead of hanging.
194
+
195
+ Set `NEXT_PUBLIC_BASE_URL` (or whatever your helpers read) to the project's
196
+ own origin so absolute URLs get built at all — an unset value that falls back
197
+ to `http://localhost:3000` also works. Where a restructure is cheap, importing
198
+ the shared function instead of fetching your own route is still the simpler
199
+ pattern; the loopback exists so you don't have to.
200
+
176
201
  ## Step 4 — swap the data layer
177
202
 
178
203
  Replace the ORM and connection strings with the project's own database:
@@ -1,7 +1,7 @@
1
- import { t as version } from "./package-CF0dX7UA.mjs";
1
+ import { t as version } from "./package-X1HbEiNB.mjs";
2
2
  import { n as siteConfig } from "./site-BUlQR117.mjs";
3
3
  import { i as latestKnownVersion, r as compareSemver } from "./version-check-CcsA1QjI.mjs";
4
- import { n as reportErrors } from "./ui-B1Px5y7q.mjs";
4
+ import { r as reportErrors } from "./ui-BUBYpqoC.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";
@@ -14,6 +14,10 @@ import { fileURLToPath } from "node:url";
14
14
  * never goes stale, with a local cache and a bundled snapshot as offline
15
15
  * fallbacks. The listing is grouped per docs-site section.
16
16
  *
17
+ * `canopy docs search <terms…>` greps the locally available corpus (cache +
18
+ * bundled snapshot) — the corpus is small enough (~60 docs) that keyword
19
+ * matching beats any index, and it works fully offline.
20
+ *
17
21
  * The platform requires a login for docs (internal platform), so fetches carry
18
22
  * the CLI token when there is one; without it we simply fall back to the
19
23
  * bundled snapshot, so `canopy docs` still works before sign-in.
@@ -140,6 +144,75 @@ function bodyFromFallback(slug) {
140
144
  }
141
145
  }
142
146
  }
147
+ const SEARCH_LIMIT = 10;
148
+ const LINE_LIMIT = 2;
149
+ const LINE_WIDTH = 120;
150
+ /** A matching body line, trimmed and windowed around the first term hit. */
151
+ function excerpt(line, matchIndex) {
152
+ const clean = line.replace(/\s+/g, " ").trim();
153
+ if (clean.length <= LINE_WIDTH) return clean;
154
+ const start = Math.max(0, Math.min(matchIndex - Math.floor(LINE_WIDTH / 3), clean.length - LINE_WIDTH));
155
+ const slice = clean.slice(start, start + LINE_WIDTH);
156
+ return `${start > 0 ? "…" : ""}${slice}${start + LINE_WIDTH < clean.length ? "…" : ""}`;
157
+ }
158
+ /**
159
+ * Keyword search over the docs corpus: case-insensitive, every term must match
160
+ * somewhere in a doc (title, summary, or body). Title and summary hits weigh
161
+ * more than body occurrences; results come back best-first.
162
+ */
163
+ function searchDocs(entries, query) {
164
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
165
+ if (terms.length === 0) return [];
166
+ const hits = [];
167
+ for (const entry of entries) {
168
+ const title = entry.title.toLowerCase();
169
+ const summary = entry.summary.toLowerCase();
170
+ const body = entry.body.toLowerCase();
171
+ let score = 0;
172
+ let matchesAll = true;
173
+ for (const term of terms) {
174
+ let termScore = 0;
175
+ if (title.includes(term)) termScore += 10;
176
+ if (summary.includes(term)) termScore += 5;
177
+ let count = 0;
178
+ for (let i = body.indexOf(term); i !== -1 && count < 20; i = body.indexOf(term, i + term.length)) count++;
179
+ termScore += count;
180
+ if (termScore === 0) {
181
+ matchesAll = false;
182
+ break;
183
+ }
184
+ score += termScore;
185
+ }
186
+ if (!matchesAll) continue;
187
+ const lines = [];
188
+ for (const line of entry.body.split("\n")) {
189
+ const lower = line.toLowerCase();
190
+ const at = terms.map((t) => lower.indexOf(t)).find((i) => i !== -1);
191
+ if (at !== void 0 && line.trim()) {
192
+ lines.push(excerpt(line, at));
193
+ if (lines.length >= LINE_LIMIT) break;
194
+ }
195
+ }
196
+ hits.push({
197
+ slug: entry.slug,
198
+ title: entry.title,
199
+ score,
200
+ lines
201
+ });
202
+ }
203
+ return hits.sort((a, b) => b.score - a.score || a.slug.localeCompare(b.slug));
204
+ }
205
+ /**
206
+ * Every topic paired with its locally available body (cache first, bundled
207
+ * snapshot second). A topic with no local body still gets searched by title
208
+ * and summary — search never hits the network per doc.
209
+ */
210
+ function searchEntries(index) {
211
+ return index.topics.map((t) => ({
212
+ ...t,
213
+ body: bodyFromFallback(t.slug) ?? ""
214
+ }));
215
+ }
143
216
  async function listIndex(refresh) {
144
217
  const { apiUrl } = siteConfig();
145
218
  if (refresh || !cacheFresh()) {
@@ -174,7 +247,7 @@ const docsCommand = defineCommand({
174
247
  topic: {
175
248
  type: "positional",
176
249
  required: false,
177
- description: "Topic to print (omit to list)"
250
+ description: "Topic to print (omit to list), or `search <terms…>`"
178
251
  },
179
252
  refresh: {
180
253
  type: "boolean",
@@ -187,6 +260,23 @@ const docsCommand = defineCommand({
187
260
  const topic = args.topic;
188
261
  const latest = latestKnownVersion();
189
262
  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`);
263
+ if (topic === "search") {
264
+ const query = args._.slice(1).join(" ").trim();
265
+ if (!query) throw new Error("Usage: canopy docs search <terms…>");
266
+ const hits = searchDocs(searchEntries(await listIndex(refresh)), query);
267
+ if (hits.length === 0) {
268
+ console.log(`No docs match "${query}". Run \`canopy docs\` to list all topics.`);
269
+ return;
270
+ }
271
+ const shown = hits.slice(0, SEARCH_LIMIT);
272
+ console.log(`${hits.length} doc${hits.length === 1 ? "" : "s"} match "${query}" — run \`canopy docs <topic>\` to read one:\n`);
273
+ for (const hit of shown) {
274
+ console.log(`${hit.slug} — ${hit.title}`);
275
+ for (const line of hit.lines) console.log(` ${line}`);
276
+ }
277
+ if (hits.length > shown.length) console.log(`\n…and ${hits.length - shown.length} more — add terms to narrow it down.`);
278
+ return;
279
+ }
190
280
  if (!topic) {
191
281
  const index = await listIndex(refresh);
192
282
  if (index.topics.length === 0) throw new Error("No docs available — check your connection, or reinstall the CLI.");
@@ -1,5 +1,5 @@
1
1
  import { a as projectDirArgs, c as resolveProject } from "./site-BUlQR117.mjs";
2
- import { c as apiRequest, i as unwrap, n as reportErrors, r as requireTty } from "./ui-B1Px5y7q.mjs";
2
+ import { a as unwrap, i as requireTty, l as apiRequest, r as reportErrors, t as formatDateTime } from "./ui-BUBYpqoC.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import * as p from "@clack/prompts";
5
5
  //#region src/egress.ts
@@ -20,7 +20,7 @@ function printList({ mode, hosts, denied, enforced }, slug) {
20
20
  console.log(`\nRecently blocked (allow with \`canopy egress allow <host>\`):`);
21
21
  for (const d of denied) {
22
22
  const why = d.reason === "rate_limited" ? "rate limited" : "blocked";
23
- console.log(` ${d.host} ×${d.count} ${why}, last ${d.last_seen}`);
23
+ console.log(` ${d.host} ×${d.count} ${why}, last ${formatDateTime(d.last_seen)}`);
24
24
  }
25
25
  }
26
26
  if (!enforced) console.error("(note: this site's target does not enforce egress yet — the policy is recorded for when it does)");
@@ -175,7 +175,7 @@ const egressCommand = defineCommand({
175
175
  console.log(`No outbound traffic recorded for ${slug} yet.`);
176
176
  return;
177
177
  }
178
- for (const v of visits) console.log(`${v.host} ×${v.count} last ${v.last_seen}`);
178
+ for (const v of visits) console.log(`${v.host} ×${v.count} last ${formatDateTime(v.last_seen)}`);
179
179
  })
180
180
  })
181
181
  }
@@ -1,6 +1,6 @@
1
- import { t as version } from "./package-CF0dX7UA.mjs";
1
+ import { t as version } from "./package-X1HbEiNB.mjs";
2
2
  import { a as projectDirArgs, c as resolveProject, n as siteConfig, u as tryReadCanopyJson } from "./site-BUlQR117.mjs";
3
- import { c as apiRequest, l as apiUpload, n as reportErrors, s as apiDownload } from "./ui-B1Px5y7q.mjs";
3
+ import { c as apiDownload, l as apiRequest, r as reportErrors, t as formatDateTime, u as apiUpload } from "./ui-BUBYpqoC.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";
@@ -140,7 +140,7 @@ const showCommand = defineCommand({
140
140
  return;
141
141
  }
142
142
  const { item, comments } = detail;
143
- console.log(`${item.kind} · ${item.status} · via ${item.source} · ${item.created_at}`);
143
+ console.log(`${item.kind} · ${item.status} · via ${item.source} · ${formatDateTime(item.created_at)}`);
144
144
  console.log(`reported by ${item.reporter_name || item.reporter_email || "unknown"}` + (item.deployment_id ? ` on ${item.deployment_id}` : "") + (item.channel === "staging" ? " (staging)" : ""));
145
145
  console.log(`\n${item.body}\n`);
146
146
  if (item.context) {
@@ -166,7 +166,7 @@ const showCommand = defineCommand({
166
166
  const author = entry.author_name || entry.author_email || "unknown";
167
167
  const move = entry.status_to ? ` [${entry.status_from} → ${entry.status_to}]` : "";
168
168
  const internal = entry.internal ? " [internal]" : "";
169
- console.log(`--- ${author} · ${entry.created_at}${move}${internal}`);
169
+ console.log(`--- ${author} · ${formatDateTime(entry.created_at)}${move}${internal}`);
170
170
  if (entry.body) console.log(entry.body);
171
171
  }
172
172
  if (args.download && shots.length > 0) {
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as version } from "./package-CF0dX7UA.mjs";
2
+ import { t as version } from "./package-X1HbEiNB.mjs";
3
3
  import { r as siteHeaderLine, t as setInvocationProfile } from "./site-BUlQR117.mjs";
4
4
  import { a as notifyOfUpdate, o as runRefresh } from "./version-check-CcsA1QjI.mjs";
5
5
  import { defineCommand, runMain } from "citty";
@@ -60,30 +60,30 @@ const main = defineCommand({
60
60
  description: "canopy — create and deploy vibecoded projects"
61
61
  },
62
62
  subCommands: {
63
- login: () => import("./login-BDTO3nVw.mjs").then((m) => m.loginCommand),
64
- whoami: () => import("./login-BDTO3nVw.mjs").then((m) => m.whoamiCommand),
65
- logout: () => import("./login-BDTO3nVw.mjs").then((m) => m.logoutCommand),
66
- create: () => import("./projects-MMbv7MmK.mjs").then((m) => m.createCommand),
67
- init: () => import("./projects-MMbv7MmK.mjs").then((m) => m.initCommand),
68
- projects: () => import("./projects-MMbv7MmK.mjs").then((m) => m.projectsCommand),
69
- delete: () => import("./projects-MMbv7MmK.mjs").then((m) => m.deleteCommand),
70
- dev: () => import("./dev-BqLvttKc.mjs").then((m) => m.devCommand),
71
- deploy: () => import("./deploy-CnxJ4TE6.mjs").then((m) => m.deployCommand),
72
- promote: () => import("./deploy-CnxJ4TE6.mjs").then((m) => m.promoteCommand),
73
- staging: () => import("./staging-D8G4amjO.mjs").then((m) => m.stagingCommand),
74
- rollback: () => import("./deploy-CnxJ4TE6.mjs").then((m) => m.rollbackCommand),
75
- download: () => import("./deploy-CnxJ4TE6.mjs").then((m) => m.downloadCommand),
76
- token: () => import("./token-BUxgRJVA.mjs").then((m) => m.tokenCommand),
77
- egress: () => import("./egress-PJp0SZy2.mjs").then((m) => m.egressCommand),
78
- schedule: () => import("./schedule-Cq9K0rnE.mjs").then((m) => m.scheduleCommand),
79
- feedback: () => import("./feedback-h_j6DBBZ.mjs").then((m) => m.feedbackCommand),
80
- db: () => import("./db-DKnJQPm5.mjs").then((m) => m.dbCommand),
81
- usage: () => import("./usage-DjNngyvy.mjs").then((m) => m.usageCommand),
82
- open: () => import("./open-C5x3tX6I.mjs").then((m) => m.openCommand),
83
- docs: () => import("./docs-BlzdYmou.mjs").then((m) => m.docsCommand),
84
- skill: () => import("./skill-DaaxgqEk.mjs").then((m) => m.skillCommand),
85
- upgrade: () => import("./upgrade-DG-7xwOs.mjs").then((m) => m.upgradeCommand),
86
- logs: () => import("./logs-C53y2eAS.mjs").then((m) => m.logsCommand)
63
+ login: () => import("./login-CqM5bZNZ.mjs").then((m) => m.loginCommand),
64
+ whoami: () => import("./login-CqM5bZNZ.mjs").then((m) => m.whoamiCommand),
65
+ logout: () => import("./login-CqM5bZNZ.mjs").then((m) => m.logoutCommand),
66
+ create: () => import("./projects-C6tqDIV_.mjs").then((m) => m.createCommand),
67
+ init: () => import("./projects-C6tqDIV_.mjs").then((m) => m.initCommand),
68
+ projects: () => import("./projects-C6tqDIV_.mjs").then((m) => m.projectsCommand),
69
+ delete: () => import("./projects-C6tqDIV_.mjs").then((m) => m.deleteCommand),
70
+ dev: () => import("./dev-DAyB0TRG.mjs").then((m) => m.devCommand),
71
+ deploy: () => import("./deploy-DLehZzUr.mjs").then((m) => m.deployCommand),
72
+ promote: () => import("./deploy-DLehZzUr.mjs").then((m) => m.promoteCommand),
73
+ staging: () => import("./staging-CL0Q2v6t.mjs").then((m) => m.stagingCommand),
74
+ rollback: () => import("./deploy-DLehZzUr.mjs").then((m) => m.rollbackCommand),
75
+ download: () => import("./deploy-DLehZzUr.mjs").then((m) => m.downloadCommand),
76
+ token: () => import("./token-HAniLS_l.mjs").then((m) => m.tokenCommand),
77
+ egress: () => import("./egress-D-Y0vANf.mjs").then((m) => m.egressCommand),
78
+ schedule: () => import("./schedule-lZA3TPTm.mjs").then((m) => m.scheduleCommand),
79
+ feedback: () => import("./feedback-Dmrn3SW3.mjs").then((m) => m.feedbackCommand),
80
+ db: () => import("./db-DUVadyyv.mjs").then((m) => m.dbCommand),
81
+ usage: () => import("./usage-XCARqs3z.mjs").then((m) => m.usageCommand),
82
+ open: () => import("./open-BXP6rJ7R.mjs").then((m) => m.openCommand),
83
+ docs: () => import("./docs-DOopUT5k.mjs").then((m) => m.docsCommand),
84
+ skill: () => import("./skill-Beg6Wyik.mjs").then((m) => m.skillCommand),
85
+ upgrade: () => import("./upgrade-D297Aze4.mjs").then((m) => m.upgradeCommand),
86
+ logs: () => import("./logs-DuQmfRoW.mjs").then((m) => m.logsCommand)
87
87
  }
88
88
  });
89
89
  if (process.argv[2] !== "upgrade") notifyOfUpdate(version);
@@ -1,6 +1,6 @@
1
- import { o as apiDataRequest } from "./ui-B1Px5y7q.mjs";
1
+ import { s as apiDataRequest } from "./ui-BUBYpqoC.mjs";
2
2
  import { n as META_COLUMNS, r as blobRouter, t as dbRouter } from "./src-Bu4Ze32I.mjs";
3
- import { n as rewriteApiPath } from "./dev-CyJT6EfQ.mjs";
3
+ import { n as rewriteApiPath } from "./dev-Bvqpc0zN.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";
@@ -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-B1Px5y7q.mjs";
2
+ import { n as openBrowser, r as reportErrors } from "./ui-BUBYpqoC.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 { c as resolveProject, o as projectFlagArgs } from "./site-BUlQR117.mjs";
2
- import { c as apiRequest, n as reportErrors, u as withContext } from "./ui-B1Px5y7q.mjs";
2
+ import { d as withContext, l as apiRequest, r as reportErrors } from "./ui-BUBYpqoC.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-BUlQR117.mjs";
2
- import { o as apiDataRequest } from "./ui-B1Px5y7q.mjs";
2
+ import { s as apiDataRequest } from "./ui-BUBYpqoC.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-B1Px5y7q.mjs";
2
+ import { n as openBrowser, r as reportErrors } from "./ui-BUBYpqoC.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.15";
2
+ var version = "0.7.17";
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-B1Px5y7q.mjs";
2
+ import { a as unwrap, i as requireTty, l as apiRequest, r as reportErrors } from "./ui-BUBYpqoC.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-BUlQR117.mjs";
2
- import { c as apiRequest, n as reportErrors, u as withContext } from "./ui-B1Px5y7q.mjs";
2
+ import { d as withContext, l as apiRequest, r as reportErrors, t as formatDateTime } from "./ui-BUBYpqoC.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  //#region src/schedule.ts
5
5
  /**
@@ -13,7 +13,7 @@ function renderSchedules(schedules) {
13
13
  const cronWidth = Math.max(...schedules.map((s) => s.cron.length));
14
14
  return schedules.map((s) => {
15
15
  const state = s.enabled ? "enabled " : "disabled";
16
- const lastRun = s.last_run_at ? `last run ${s.last_run_at} ${s.last_status ?? ""}`.trimEnd() + (s.last_error ? ` (${s.last_error})` : "") : "never run";
16
+ const lastRun = s.last_run_at ? `last run ${formatDateTime(s.last_run_at)} ${s.last_status ?? ""}`.trimEnd() + (s.last_error ? ` (${s.last_error})` : "") : "never run";
17
17
  return `${s.name.padEnd(nameWidth)} ${s.cron.padEnd(cronWidth)} ${state} ${lastRun}`;
18
18
  }).join("\n");
19
19
  }
@@ -4,8 +4,9 @@ description: >-
4
4
  Build, run, and deploy projects on Canopy — Monks' managed platform for
5
5
  vibecoded apps (hosting + database + blob storage + AI). Use this whenever the
6
6
  user wants to create a Canopy project, run/deploy one, manage its data or
7
- deploy tokens, or asks how to use the `canopy` CLI. All platform docs are
8
- available offline via `canopy docs`.
7
+ deploy tokens, work a project's feedback inbox (reports/issues/bugs filed by
8
+ users — look one up, investigate, fix, resolve), or asks how to use the
9
+ `canopy` CLI. All platform docs are available offline via `canopy docs`.
9
10
  ---
10
11
 
11
12
  # Canopy
@@ -71,8 +72,9 @@ The CLI ships the full documentation and keeps it fresh from the platform. List
71
72
  the topics and read the relevant one _before_ running unfamiliar commands:
72
73
 
73
74
  ```bash
74
- canopy docs # list all topics
75
- canopy docs deploy # print one topic
75
+ canopy docs # list all topics
76
+ canopy docs deploy # print one topic
77
+ canopy docs search egress allowlist # find the topics that mention all terms
76
78
  ```
77
79
 
78
80
  The listing is grouped by section: guides (`getting-started`, `deploy`, `dev`,
@@ -97,9 +99,31 @@ and never go stale.
97
99
  - Open the live site: `canopy open`
98
100
  - Manage data: `canopy db pull | snapshot | snapshots | restore | migrate`
99
101
  - Work bug reports & feedback: `canopy feedback list | show | comment | status | report`
100
- — see `canopy docs platform-feedback` for the triage and reporting loops
102
+ — see [Working feedback reports](#working-feedback-reports-issues) below
101
103
  - CI credentials: `canopy token create | list | revoke`
102
104
 
105
+ ## Working feedback reports (issues)
106
+
107
+ Project members file bugs & feedback through the in-app widget; the CLI is how
108
+ an agent works them. Report, issue, feedback item — same thing. Read
109
+ `canopy docs platform-feedback` before triaging; it defines the loop. The
110
+ short version:
111
+
112
+ - `canopy feedback list --status investigate` shows what nobody has looked at;
113
+ `canopy feedback show <id>` has the body, captured context, and comment
114
+ trail. The context includes the deploy version that served the bug —
115
+ reproduce against that, not whatever is live now.
116
+ - Investigated ≠ fixed: write findings with
117
+ `canopy feedback status <id> investigated --comment "<root cause>"`.
118
+ - **`accepted` means a person said "build this."** Never set it on your own —
119
+ only when the user you're working with says to build the thing.
120
+ - Put tech-heavy notes (stack traces, table names, root-cause detail) in an
121
+ internal comment (`canopy feedback comment <id> "…" --internal`) so they
122
+ never reach the reporter.
123
+ - Resolve only once the fix is actually live, and name the version:
124
+ `canopy feedback status <id> resolved --comment "fixed in v42"`. Attach
125
+ proof when it's visual: `canopy feedback attach <id> ./after-fix.png`.
126
+
103
127
  ## Custom tables & migrations (devs; Drizzle recommended)
104
128
 
105
129
  The document API (`canopy.db.collection(…)`) needs no schema — prefer it. When
@@ -129,7 +153,7 @@ for it only when the app is genuinely trivial** — a page or two with light SDK
129
153
  use. Its SDK is served untyped from the platform (`/_api/data/v1/sdk.js`): no
130
154
  autocomplete, no type-checking, mistakes only show up in the browser.
131
155
 
132
- Prefer **`spa`** the moment either is true — which covers most real projects:
156
+ Prefer **`spa`** over **`static`** the moment either is true — which covers most real projects:
133
157
 
134
158
  - **You lean on the SDK** (`db` queries, `blob`, `ai` — anything past a stray
135
159
  `auth.user()`). `spa` installs the SDK from npm (`@mediamonks/canopy`), fully
@@ -1,18 +1,23 @@
1
- import { t as version } from "./package-CF0dX7UA.mjs";
2
- import { n as reportErrors } from "./ui-B1Px5y7q.mjs";
1
+ import { t as version } from "./package-X1HbEiNB.mjs";
2
+ import { r as reportErrors } from "./ui-BUBYpqoC.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import { cpSync, existsSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
5
+ import { homedir } from "node:os";
5
6
  import { join, relative } from "node:path";
6
7
  import { fileURLToPath } from "node:url";
7
8
  //#region src/skill.ts
8
9
  /**
9
- * canopy skill install — write the Canopy agent skill into the current repo.
10
+ * canopy skill install — write the Canopy agent skill for coding agents.
10
11
  *
11
- * To avoid vendor lock-in the skill lives in the tool-neutral `.agents/skills/`
12
- * tree, and `.claude/skills/canopy` is a symlink to it (so Claude Code picks it
13
- * up while other agents can read `.agents/`). The skill files are bundled with
14
- * the CLI (dist/skill in the published package, packages/cli/skill in dev), so
15
- * `canopy upgrade` re-runs this to keep the installed skill in sync.
12
+ * Installs globally by default (`~/.agents/skills/canopy`, with
13
+ * `~/.claude/skills/canopy` symlinked to it — Claude Code's user scope, so the
14
+ * skill applies in every repo). `--repo` writes the same layout into the
15
+ * current repo instead, for teams that want the skill committed with a
16
+ * project. To avoid vendor lock-in the skill lives in the tool-neutral
17
+ * `.agents/skills/` tree either way, and `.claude` is only a symlink. The
18
+ * skill files are bundled with the CLI (dist/skill in the published package,
19
+ * packages/cli/skill in dev), so `canopy upgrade` re-runs this to keep every
20
+ * existing install — global and repo — in sync.
16
21
  */
17
22
  const SKILL_NAME = "canopy";
18
23
  const AGENTS_REL = join(".agents", "skills", SKILL_NAME);
@@ -64,18 +69,24 @@ function skillInstalled(root) {
64
69
  const skillCommand = defineCommand({
65
70
  meta: {
66
71
  name: "skill",
67
- description: "Manage the Canopy agent skill in this repo"
72
+ description: "Manage the Canopy agent skill"
68
73
  },
69
74
  subCommands: { install: () => defineCommand({
70
75
  meta: {
71
76
  name: "install",
72
- description: "Install/refresh the Canopy skill (.agents + .claude)"
77
+ description: "Install/refresh the Canopy skill — global by default, --repo for this repo"
73
78
  },
74
- run: reportErrors("skill install", async () => {
75
- const { claudeMode } = installSkill(process.cwd());
76
- console.log(`Installed the Canopy skill (v${version}):`);
77
- console.log(` .agents/skills/${SKILL_NAME}/ (source of truth)`);
78
- console.log(claudeMode === "symlink" ? ` .claude/skills/${SKILL_NAME} → symlink to .agents` : ` .claude/skills/${SKILL_NAME} (copy — symlinks unavailable here)`);
79
+ args: { repo: {
80
+ type: "boolean",
81
+ description: "Install into the current repo (.agents + .claude here) instead of globally"
82
+ } },
83
+ run: reportErrors("skill install", async ({ args }) => {
84
+ const { claudeMode } = installSkill(args.repo ? process.cwd() : homedir());
85
+ const where = args.repo ? "into this repo" : "globally (~)";
86
+ const prefix = args.repo ? "" : "~/";
87
+ console.log(`Installed the Canopy skill ${where} (v${version}):`);
88
+ console.log(` ${prefix}.agents/skills/${SKILL_NAME}/ (source of truth)`);
89
+ console.log(claudeMode === "symlink" ? ` ${prefix}.claude/skills/${SKILL_NAME} → symlink to .agents` : ` ${prefix}.claude/skills/${SKILL_NAME} (copy — symlinks unavailable here)`);
79
90
  console.log("\nYour agent can now use Canopy. Try: canopy docs");
80
91
  })
81
92
  }) }
@@ -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, n as reportErrors, r as requireTty, u as withContext } from "./ui-B1Px5y7q.mjs";
2
+ import { a as unwrap, d as withContext, i as requireTty, l as apiRequest, r as reportErrors, t as formatDateTime } from "./ui-BUBYpqoC.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import * as p from "@clack/prompts";
5
5
  //#region src/staging.ts
@@ -62,7 +62,7 @@ const stagingCommand = defineCommand({
62
62
  message: "Version to serve on staging",
63
63
  options: deploys.map((deploy) => ({
64
64
  value: deploy.version,
65
- label: `v${deploy.version} ${deploy.created_at} (${deploy.source})`,
65
+ label: `v${deploy.version} ${formatDateTime(deploy.created_at)} (${deploy.source})`,
66
66
  hint: deploy.version === staged ? "staged" : deploy.version === live ? "live" : void 0
67
67
  }))
68
68
  }));
@@ -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-B1Px5y7q.mjs";
2
+ import { l as apiRequest, r as reportErrors, t as formatDateTime } from "./ui-BUBYpqoC.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  const tokenCommand = defineCommand({
5
5
  meta: {
@@ -42,7 +42,7 @@ const tokenCommand = defineCommand({
42
42
  console.log(`No deploy tokens for ${slug} — create one with \`canopy token create\`.`);
43
43
  return;
44
44
  }
45
- for (const t of tokens) console.log(`${t.id} ${t.name ?? "(unnamed)"} created ${t.created_at} by ${t.created_by ?? "?"}`);
45
+ for (const t of tokens) console.log(`${t.id} ${t.name ?? "(unnamed)"} created ${formatDateTime(t.created_at)} by ${t.created_by ?? "?"}`);
46
46
  })
47
47
  }),
48
48
  revoke: defineCommand({
@@ -383,6 +383,18 @@ function reportErrors(name, action) {
383
383
  function requireTty(hint) {
384
384
  if (!process.stdin.isTTY) throw new Error(`This needs an interactive terminal — ${hint}`);
385
385
  }
386
+ /**
387
+ * API timestamps are UTC — ISO with a Z suffix, though a lagging API site may
388
+ * still send the bare "YYYY-MM-DD HH:MM:SS" form. Parse either as UTC and
389
+ * render compact local wall-clock time ("2026-08-25 14:23").
390
+ */
391
+ function formatDateTime(raw) {
392
+ const hasZone = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(raw);
393
+ const date = new Date(hasZone ? raw : raw.replace(" ", "T") + "Z");
394
+ if (Number.isNaN(date.getTime())) return raw;
395
+ const pad = (n) => String(n).padStart(2, "0");
396
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
397
+ }
386
398
  /** Open a URL in the default browser, fire-and-forget. */
387
399
  function openBrowser(url) {
388
400
  spawn(process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open", [url], {
@@ -392,4 +404,4 @@ function openBrowser(url) {
392
404
  }).on("error", () => {}).unref();
393
405
  }
394
406
  //#endregion
395
- export { ApiError as a, apiRequest as c, unwrap as i, apiUpload as l, reportErrors as n, apiDataRequest as o, requireTty as r, apiDownload as s, openBrowser as t, withContext as u };
407
+ export { unwrap as a, apiDownload as c, withContext as d, requireTty as i, apiRequest as l, openBrowser as n, ApiError as o, reportErrors as r, apiDataRequest as s, formatDateTime as t, apiUpload as u };
@@ -1,5 +1,5 @@
1
1
  import { t as PACKAGE_NAME } from "./version-check-CcsA1QjI.mjs";
2
- import { n as reportErrors } from "./ui-B1Px5y7q.mjs";
2
+ import { r as reportErrors } from "./ui-BUBYpqoC.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import { spawn } from "node:child_process";
5
5
  import { fileURLToPath } from "node:url";
@@ -112,16 +112,27 @@ 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-DaaxgqEk.mjs");
116
- const refreshSkill = skillInstalled(process.cwd());
115
+ const { skillInstalled } = await import("./skill-Beg6Wyik.mjs");
116
+ const { homedir } = await import("node:os");
117
+ const refreshGlobal = skillInstalled(homedir());
118
+ const refreshRepo = process.cwd() !== homedir() && skillInstalled(process.cwd());
117
119
  console.log(`Upgrading via ${command}: ${command} ${cmd.join(" ")}`);
118
120
  await run(command, cmd);
119
121
  console.log(`\nDone. Run \`canopy --version\` to confirm.`);
120
- if (refreshSkill) {
122
+ const warnSkill = (label) => (err) => {
123
+ console.warn(`Could not auto-refresh the ${label} skill (${err instanceof Error ? err.message : String(err)}). Run \`canopy skill install\` yourself.`);
124
+ };
125
+ if (refreshGlobal) {
126
+ console.log("Refreshing the global Canopy agent skill…");
127
+ await run("canopy", ["skill", "install"]).catch(warnSkill("global"));
128
+ }
129
+ if (refreshRepo) {
121
130
  console.log("Refreshing the Canopy agent skill in this repo…");
122
- await run("canopy", ["skill", "install"]).catch((err) => {
123
- console.warn(`Could not auto-refresh the skill (${err instanceof Error ? err.message : String(err)}). Run \`canopy skill install\` yourself.`);
124
- });
131
+ await run("canopy", [
132
+ "skill",
133
+ "install",
134
+ "--repo"
135
+ ]).catch(warnSkill("repo"));
125
136
  }
126
137
  })
127
138
  });
@@ -1,5 +1,5 @@
1
1
  import { c as resolveProject, o as projectFlagArgs } from "./site-BUlQR117.mjs";
2
- import { c as apiRequest, n as reportErrors, u as withContext } from "./ui-B1Px5y7q.mjs";
2
+ import { d as withContext, l as apiRequest, r as reportErrors } from "./ui-BUBYpqoC.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.15",
3
+ "version": "0.7.17",
4
4
  "description": "Canopy CLI — create, develop and deploy Canopy projects.",
5
5
  "bin": {
6
6
  "canopy": "./dist/index.mjs"
@@ -20,8 +20,8 @@
20
20
  "esbuild": "^0.28.1",
21
21
  "hono": "^4.8.0",
22
22
  "vite-server": "npm:vite@^7",
23
- "@mediamonks/canopy-data-plane": "0.0.4",
24
- "@mediamonks/canopy": "0.0.5"
23
+ "@mediamonks/canopy": "0.0.5",
24
+ "@mediamonks/canopy-data-plane": "0.0.4"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^26.1.1",
@@ -1,2 +0,0 @@
1
- import { t as devCommand } from "./dev-CyJT6EfQ.mjs";
2
- export { devCommand };