@mediamonks/canopy-cli 0.7.37 → 0.7.39

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 (44) hide show
  1. package/README.md +5 -2
  2. package/dist/{ai-CjvrcBIz.mjs → ai-EyrXJC5z.mjs} +2 -2
  3. package/dist/{api-BPEYm9ts.mjs → api-ArfD6-t5.mjs} +40 -5
  4. package/dist/{browse-CBelCgnW.mjs → browse-DEhnJAPT.mjs} +1 -1
  5. package/dist/{db-CbqgjfYz.mjs → db-DGrCgj-R.mjs} +3 -3
  6. package/dist/{deploy-CqCyry4o.mjs → deploy-B7KItUnh.mjs} +3 -3
  7. package/dist/{dev-BtnEuAnS.mjs → dev-BMWj4NDZ.mjs} +5 -5
  8. package/dist/dev-BdSq6CGD.mjs +2 -0
  9. package/dist/docs/cli-create.md +2 -0
  10. package/dist/docs/cli-feedback.md +27 -1
  11. package/dist/docs/cli-projects.md +2 -0
  12. package/dist/docs/cli-secrets.md +2 -1
  13. package/dist/docs/cli-template.md +47 -0
  14. package/dist/docs/index.json +10 -0
  15. package/dist/docs/platform-schedules.md +21 -0
  16. package/dist/docs/templates.md +84 -0
  17. package/dist/{docs-DvCUaI1O.mjs → docs-Bplp4sOY.mjs} +7 -3
  18. package/dist/{egress-ButFsQ2N.mjs → egress-CzxRD6Tp.mjs} +2 -2
  19. package/dist/{feedback-CIjjiEaj.mjs → feedback-DFK0TXlt.mjs} +358 -14
  20. package/dist/index.mjs +31 -30
  21. package/dist/{local-BfqTheMP.mjs → local-BnfAEwso.mjs} +2 -2
  22. package/dist/{login-CtDWXICZ.mjs → login-B2FJGIrD.mjs} +24 -7
  23. package/dist/{logs-BltUi4Gp.mjs → logs-aJwWWaMx.mjs} +2 -2
  24. package/dist/{mcp-DTUJV4FT.mjs → mcp-DuT6_wei.mjs} +2 -2
  25. package/dist/{migrations-CF1iNZ7F.mjs → migrations-CG_O5Ghw.mjs} +1 -1
  26. package/dist/{open-DXo60Ikf.mjs → open-COFMburt.mjs} +1 -1
  27. package/dist/{package-BY69Tjq_.mjs → package-CJJpjVh_.mjs} +1 -1
  28. package/dist/project-template-DlzMx8Oj.mjs +2 -0
  29. package/dist/project-template-DtTfXyLI.mjs +125 -0
  30. package/dist/{projects-DPGRI-SI.mjs → projects-B3Wi01lw.mjs} +74 -8
  31. package/dist/{repo-CKIQrrHK.mjs → repo-4x-CJwCp.mjs} +22 -16
  32. package/dist/repo-CmbakFVi.mjs +2 -0
  33. package/dist/{schedule-DdMq5eXC.mjs → schedule-CbaDIJXr.mjs} +2 -2
  34. package/dist/{secrets-BQYaPSOT.mjs → secrets-CmY4dUHz.mjs} +6 -5
  35. package/dist/skill/canopy/SKILL.md +7 -4
  36. package/dist/{skill-B96cKy8K.mjs → skill-C2lGGz9n.mjs} +2 -2
  37. package/dist/{staging-CU4QrzFX.mjs → staging-ypR79MlD.mjs} +2 -2
  38. package/dist/{token-BWnLsWLg.mjs → token-CDrHQ5Xb.mjs} +2 -2
  39. package/dist/{ui-CHfKV6lG.mjs → ui-CaHgvvkJ.mjs} +1 -1
  40. package/dist/{upgrade-SfxmPh3F.mjs → upgrade-Caj1RsA7.mjs} +2 -2
  41. package/dist/{usage-Cqqvnzrf.mjs → usage-CP8rBVZi.mjs} +2 -2
  42. package/package.json +1 -1
  43. package/dist/dev-D_qzeLng.mjs +0 -2
  44. package/dist/repo-DvrIsubH.mjs +0 -2
@@ -0,0 +1,125 @@
1
+ import { a as projectDirArgs, c as resolveProject } from "./site-DM2J10CB.mjs";
2
+ import { a as apiRequest, l as withContext } from "./api-ArfD6-t5.mjs";
3
+ import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-CaHgvvkJ.mjs";
4
+ import { defineCommand } from "citty";
5
+ import * as p from "@clack/prompts";
6
+ /** Plain words for the API's template errors. */
7
+ const TEMPLATE_FIXES = {
8
+ template_not_found: "No template by that name on this site. `canopy projects --templates` lists the templates you can start from.",
9
+ template_empty: "The template's repository has no code yet. Ask its owner to push it first.",
10
+ template_too_large: `The template's files are larger than the 32 MiB a copy can hold. Ask its owner to remove large files from the repository.`,
11
+ repo_required: "A template needs a Canopy git repository. Create one with `canopy repo create`, push the code, then try again.",
12
+ repo_empty: "Push the project's code to its repository first, then try again.",
13
+ owner_only: "Only the project owner can make it a template, or stop it being one.",
14
+ repos_not_configured: "Templates need Canopy git repositories, which are not available on this site yet."
15
+ };
16
+ /** What the new project gets, one line each — shown before creating. */
17
+ function describeCopy(copy) {
18
+ const lines = ["The code in its repository, as one fresh commit (no history)."];
19
+ if (copy.egress_mode === "open") lines.push("Outbound access: open (the backend may call any host).");
20
+ else if (copy.egress_allowlist.length) lines.push(`Outbound hosts allowed: ${copy.egress_allowlist.join(", ")}.`);
21
+ const loosened = [
22
+ !copy.csp_enabled && "Content-Security-Policy off",
23
+ copy.allow_framing && "other sites may frame the app",
24
+ copy.browser_db_access !== "full" && `browser database access: ${copy.browser_db_access}`,
25
+ !copy.feedback_widget && "feedback widget hidden"
26
+ ].filter(Boolean);
27
+ if (loosened.length) lines.push(`Page settings: ${loosened.join("; ")}.`);
28
+ if (copy.google_bundles.length) lines.push(`Google access it asks visitors for: ${copy.google_bundles.join(", ")}.`);
29
+ if (copy.schedules.length) lines.push(`Schedules, turned off: ${copy.schedules.map((s) => `${s.name} (${s.cron})`).join(", ")}.`);
30
+ if (copy.secrets.length) lines.push(`Secret names to fill in (no values): ${copy.secrets.join(", ")}.`);
31
+ lines.push("Not copied: its data, files, members, password, budgets or history.");
32
+ return lines;
33
+ }
34
+ /** What is left to do once the project exists. */
35
+ function nextSteps(slug, copy) {
36
+ const steps = [`cd ${slug}`];
37
+ for (const name of copy.secrets) steps.push(`canopy secrets set ${name}`);
38
+ steps.push("canopy deploy");
39
+ for (const s of copy.schedules) steps.push(`canopy schedule add ${s.name} "${s.cron}" (to turn it on)`);
40
+ if (copy.google_restricted.length) steps.push(`Ask a Canopy admin to unlock restricted Google access (${copy.google_restricted.join(", ")}), then turn it on in the dashboard.`);
41
+ return steps;
42
+ }
43
+ function fetchTemplate(slug) {
44
+ return withContext(apiRequest("GET", `/projects/${slug}/template`), {
45
+ action: `read template ${slug}`,
46
+ fixes: TEMPLATE_FIXES
47
+ });
48
+ }
49
+ const templateCommand = defineCommand({
50
+ meta: {
51
+ name: "template",
52
+ description: "Offer a project as a template (mark, unmark, show)"
53
+ },
54
+ subCommands: {
55
+ mark: defineCommand({
56
+ meta: {
57
+ name: "mark",
58
+ description: "Let anyone on this site start a project from this one"
59
+ },
60
+ args: {
61
+ ...projectDirArgs,
62
+ yes: {
63
+ type: "boolean",
64
+ description: "Skip the confirmation",
65
+ default: false
66
+ }
67
+ },
68
+ run: reportErrors("template mark", async ({ args }) => {
69
+ const { slug } = resolveProject(args);
70
+ if (!args.yes) {
71
+ requireTty("pass --yes to confirm.");
72
+ p.log.info(`Everyone on this site will be able to start a project of their own with a copy of ${slug}'s current code in its repository, plus its setup (outbound hosts, page settings, schedule and secret names). Its data, secret values and members are never copied.`);
73
+ if (!unwrap(await p.confirm({
74
+ message: `Make ${slug} a template?`,
75
+ initialValue: false
76
+ }))) {
77
+ p.cancel("Not marked.");
78
+ return;
79
+ }
80
+ }
81
+ if ((await withContext(apiRequest("PUT", `/projects/${slug}/template`, { enabled: true }), {
82
+ action: `make ${slug} a template`,
83
+ fixes: TEMPLATE_FIXES
84
+ })).warnings.includes("no_migrations")) p.log.warn("canopy.json sets no \"migrations\", so new projects start with an empty database: none of its tables come along. Add migrations to the repository to fix that.");
85
+ p.log.success(`${slug} is a template. Others start from it with \`canopy create <name> --from ${slug}\`.`);
86
+ })
87
+ }),
88
+ unmark: defineCommand({
89
+ meta: {
90
+ name: "unmark",
91
+ description: "Stop new projects starting from this one"
92
+ },
93
+ args: { ...projectDirArgs },
94
+ run: reportErrors("template unmark", async ({ args }) => {
95
+ const { slug } = resolveProject(args);
96
+ await withContext(apiRequest("PUT", `/projects/${slug}/template`, { enabled: false }), {
97
+ action: `stop ${slug} being a template`,
98
+ fixes: TEMPLATE_FIXES
99
+ });
100
+ p.log.success(`${slug} is no longer a template. Projects already started from it are not affected.`);
101
+ })
102
+ }),
103
+ show: defineCommand({
104
+ meta: {
105
+ name: "show",
106
+ description: "Show what a new project gets from a template"
107
+ },
108
+ args: { slug: {
109
+ type: "positional",
110
+ description: "Template slug",
111
+ required: true
112
+ } },
113
+ run: reportErrors("template show", async ({ args }) => {
114
+ const preview = await fetchTemplate(args.slug);
115
+ const t = preview.template;
116
+ console.log(`${t.name} (${t.slug}) — by ${t.owner.name} <${t.owner.email}>`);
117
+ if (t.description) console.log(t.description);
118
+ console.log("");
119
+ for (const line of describeCopy(preview.copies)) console.log(`- ${line}`);
120
+ })
121
+ })
122
+ }
123
+ });
124
+ //#endregion
125
+ export { templateCommand as a, nextSteps as i, describeCopy as n, fetchTemplate as r, TEMPLATE_FIXES as t };
@@ -1,7 +1,8 @@
1
1
  import { n as siteConfig, s as projectUrl, u as tryReadCanopyJson } from "./site-DM2J10CB.mjs";
2
- import { i as apiRequest } from "./api-BPEYm9ts.mjs";
3
- import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-CHfKV6lG.mjs";
4
- import { r as offerRepoOnCreate } from "./repo-CKIQrrHK.mjs";
2
+ import { a as apiRequest, l as withContext } from "./api-ArfD6-t5.mjs";
3
+ import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-CaHgvvkJ.mjs";
4
+ import { i as nextSteps, n as describeCopy, r as fetchTemplate, t as TEMPLATE_FIXES } from "./project-template-DtTfXyLI.mjs";
5
+ import { i as offerRepoOnCreate, n as cloneProjectRepo } from "./repo-4x-CJwCp.mjs";
5
6
  import { defineCommand } from "citty";
6
7
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
7
8
  import { join, relative, resolve, sep } from "node:path";
@@ -111,6 +112,10 @@ const createCommand = defineCommand({
111
112
  type: "boolean",
112
113
  description: "Also create a git repository for the project on Canopy",
113
114
  negativeDescription: "Don't create a git repository (no question asked)"
115
+ },
116
+ from: {
117
+ type: "string",
118
+ description: "Start from a template project: a copy of its code and setup (`canopy projects --templates`)"
114
119
  }
115
120
  },
116
121
  run: reportErrors("create", async ({ args }) => {
@@ -134,6 +139,10 @@ const createCommand = defineCommand({
134
139
  }
135
140
  })).trim();
136
141
  slug ||= slugify(name);
142
+ if (args.from) {
143
+ await createFromTemplate(name, slug, args.from);
144
+ return;
145
+ }
137
146
  const scaffold = shouldScaffoldCreate(args.scaffold);
138
147
  let template = TEMPLATES.find((t) => t.id === (args.template ?? "static"));
139
148
  if (!template) throw new Error(`Unknown template "${args.template}" — available: ${TEMPLATES.map((t) => t.id).join(", ")}.`);
@@ -188,6 +197,45 @@ const createCommand = defineCommand({
188
197
  }
189
198
  })
190
199
  });
200
+ /**
201
+ * `canopy create --from` (project-templates): show what the new project
202
+ * gets, create it with a copy of the template, clone it, list what's left.
203
+ */
204
+ async function createFromTemplate(name, slug, from) {
205
+ const targetDir = resolve(slug);
206
+ if (existsSync(targetDir)) throw new Error(`Directory ./${slug} already exists — remove it, or pick another slug.`);
207
+ const preview = await fetchTemplate(from);
208
+ p.note(describeCopy(preview.copies).join("\n"), `From ${preview.template.name} (by ${preview.template.owner.name}), ${slug} gets`);
209
+ if (process.stdin.isTTY) {
210
+ if (!unwrap(await p.confirm({
211
+ message: `Create ${slug} from ${from}?`,
212
+ initialValue: true
213
+ }))) {
214
+ p.cancel("Nothing created.");
215
+ return;
216
+ }
217
+ }
218
+ const spin = p.spinner();
219
+ spin.start(`Creating ${slug} from ${from}…`);
220
+ let project;
221
+ try {
222
+ ({project} = await withContext(apiRequest("POST", "/projects", {
223
+ slug,
224
+ name,
225
+ from
226
+ }), {
227
+ action: `create ${slug} from ${from}`,
228
+ fixes: TEMPLATE_FIXES
229
+ }));
230
+ } catch (err) {
231
+ spin.error("Create failed.");
232
+ throw err;
233
+ }
234
+ spin.stop(`Created ${project.name} — ${projectUrl(project.slug)}`);
235
+ await cloneProjectRepo(slug, targetDir);
236
+ p.note(nextSteps(slug, preview.copies).join("\n"), `Cloned into ./${slug}/ — next`);
237
+ p.outro(`Live URL (after first deploy): ${projectUrl(project.slug)}`);
238
+ }
191
239
  /** Empty enough to scaffold into: missing, or holding only dotfiles (.git, .agents…). */
192
240
  function isEmptyish(dir) {
193
241
  if (!existsSync(dir)) return true;
@@ -294,13 +342,31 @@ const projectsCommand = defineCommand({
294
342
  name: "projects",
295
343
  description: "List your projects"
296
344
  },
297
- args: { all: {
298
- type: "boolean",
299
- default: false,
300
- description: "Include projects you can't edit"
301
- } },
345
+ args: {
346
+ all: {
347
+ type: "boolean",
348
+ default: false,
349
+ description: "Include projects you can't edit"
350
+ },
351
+ templates: {
352
+ type: "boolean",
353
+ default: false,
354
+ description: "List the templates anyone can start a project from"
355
+ }
356
+ },
302
357
  run: reportErrors("projects", async ({ args }) => {
303
358
  const { projects, is_admin: isAdmin } = await apiRequest("GET", "/projects");
359
+ if (args.templates) {
360
+ const templates = projects.filter((project) => project.is_template);
361
+ if (templates.length === 0) {
362
+ console.log("No templates on this site yet.");
363
+ return;
364
+ }
365
+ const width = Math.max(...templates.map((project) => project.slug.length));
366
+ for (const project of templates) console.log(`${project.slug.padEnd(width)} ${project.name} (${project.owner_email})`);
367
+ console.log(`\nStart from one with \`canopy create <name> --from <template>\`.`);
368
+ return;
369
+ }
304
370
  const mine = args.all || isAdmin ? projects : projects.filter((project) => project.role);
305
371
  if (mine.length === 0) {
306
372
  console.log(projects.length === 0 ? "No projects yet — run `canopy create`." : `No projects of your own yet — run \`canopy create\`, or --all to list all ${projects.length}.`);
@@ -1,6 +1,6 @@
1
1
  import { a as projectDirArgs, c as resolveProject, n as siteConfig, u as tryReadCanopyJson } from "./site-DM2J10CB.mjs";
2
- import { i as apiRequest, o as withContext } from "./api-BPEYm9ts.mjs";
3
- import { a as unwrap, i as requireTty, r as reportErrors, t as formatDateTime } from "./ui-CHfKV6lG.mjs";
2
+ import { a as apiRequest, l as withContext, n as agentHeaders } from "./api-ArfD6-t5.mjs";
3
+ import { a as unwrap, i as requireTty, r as reportErrors, t as formatDateTime } from "./ui-CaHgvvkJ.mjs";
4
4
  import { a as parseCredentialInput, i as helperCommand, o as runCredentialHelper, r as fileTokenCache, t as RepoCredentialError } from "./repo-credential-CNDRM-tG.mjs";
5
5
  import { defineCommand } from "citty";
6
6
  import { existsSync, realpathSync } from "node:fs";
@@ -268,20 +268,23 @@ const cloneCommand = defineCommand({
268
268
  }
269
269
  },
270
270
  run: reportErrors("repo clone", async ({ args }) => {
271
- const slug = args.slug;
272
- const repo = requireCanopyRepo(slug, await fetchRepo(slug));
273
- await requireGit();
274
- const target = resolve(args.target ?? slug);
275
- const code = await gitInteractive([
276
- "clone",
277
- ...credentialConfig(repo.remote, currentProfile(), slug).flatMap(([key, values]) => values.flatMap((value) => ["-c", `${key}=${value}`])),
278
- repo.remote,
279
- target
280
- ]);
281
- if (code !== 0) throw new Error(`git clone failed (exit ${code}).`);
282
- p.log.success(`Cloned ${slug} into ${target}.`);
271
+ const target = await cloneProjectRepo(args.slug, resolve(args.target ?? args.slug));
272
+ p.log.success(`Cloned ${args.slug} into ${target}.`);
283
273
  })
284
274
  });
275
+ /** Clone a project's Canopy repository into `target`, credential helper included. */
276
+ async function cloneProjectRepo(slug, target) {
277
+ const repo = requireCanopyRepo(slug, await fetchRepo(slug));
278
+ await requireGit();
279
+ const code = await gitInteractive([
280
+ "clone",
281
+ ...credentialConfig(repo.remote, currentProfile(), slug).flatMap(([key, values]) => values.flatMap((value) => ["-c", `${key}=${value}`])),
282
+ repo.remote,
283
+ target
284
+ ]);
285
+ if (code !== 0) throw new Error(`git clone failed (exit ${code}).`);
286
+ return target;
287
+ }
285
288
  const deleteCommand = defineCommand({
286
289
  meta: {
287
290
  name: "delete",
@@ -319,7 +322,10 @@ async function mintRepoToken(profile, slug) {
319
322
  const path = `/projects/${encodeURIComponent(slug)}/repo/token`;
320
323
  const res = await fetch(`${config.apiUrl}/control/v1${path}`, {
321
324
  method: "POST",
322
- headers: { authorization: `Bearer ${config.token}` }
325
+ headers: {
326
+ authorization: `Bearer ${config.token}`,
327
+ ...agentHeaders()
328
+ }
323
329
  }).catch(() => null);
324
330
  if (!res) throw new RepoCredentialError(`could not reach Canopy (${config.apiUrl}).`);
325
331
  if (res.status === 201) return await res.json();
@@ -420,4 +426,4 @@ async function offerRepoOnCreate(slug, dir, flag) {
420
426
  }
421
427
  }
422
428
  //#endregion
423
- export { repoCommand as i, connectFolder as n, offerRepoOnCreate as r, cliInvocation as t };
429
+ export { repoCommand as a, offerRepoOnCreate as i, cloneProjectRepo as n, connectFolder as r, cliInvocation as t };
@@ -0,0 +1,2 @@
1
+ import { a as repoCommand } from "./repo-4x-CJwCp.mjs";
2
+ export { repoCommand };
@@ -1,6 +1,6 @@
1
1
  import { c as resolveProject, o as projectFlagArgs } from "./site-DM2J10CB.mjs";
2
- import { i as apiRequest, o as withContext } from "./api-BPEYm9ts.mjs";
3
- import { r as reportErrors, t as formatDateTime } from "./ui-CHfKV6lG.mjs";
2
+ import { a as apiRequest, l as withContext } from "./api-ArfD6-t5.mjs";
3
+ import { r as reportErrors, t as formatDateTime } from "./ui-CaHgvvkJ.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  //#region src/schedule.ts
6
6
  /**
@@ -1,6 +1,6 @@
1
1
  import { a as projectDirArgs, c as resolveProject } from "./site-DM2J10CB.mjs";
2
- import { i as apiRequest, o as withContext } from "./api-BPEYm9ts.mjs";
3
- import { a as unwrap, r as reportErrors, t as formatDateTime } from "./ui-CHfKV6lG.mjs";
2
+ import { a as apiRequest, l as withContext } from "./api-ArfD6-t5.mjs";
3
+ import { a as unwrap, r as reportErrors, t as formatDateTime } from "./ui-CaHgvvkJ.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  import * as p from "@clack/prompts";
6
6
  //#region src/secrets.ts
@@ -52,13 +52,14 @@ const secretsCommand = defineCommand({
52
52
  args: { ...projectDirArgs },
53
53
  run: reportErrors("secrets list", async ({ args }) => {
54
54
  const { slug } = resolveProject(args);
55
- const { secrets } = await apiRequest("GET", `/projects/${slug}/secrets`);
56
- if (secrets.length === 0) {
55
+ const { secrets, missing = [] } = await apiRequest("GET", `/projects/${slug}/secrets`);
56
+ if (secrets.length === 0 && missing.length === 0) {
57
57
  console.log(`No secrets set for ${slug} — add one with \`canopy secrets set NAME\`.`);
58
58
  return;
59
59
  }
60
- const width = Math.max(...secrets.map((s) => s.key.length));
60
+ const width = Math.max(...secrets.map((s) => s.key.length), ...missing.map((m) => m.length));
61
61
  for (const s of secrets) console.log(`${s.key.padEnd(width)} updated ${formatDateTime(s.updated_at)}`);
62
+ for (const name of missing) console.log(`${name.padEnd(width)} missing — set it with \`canopy secrets set ${name}\``);
62
63
  })
63
64
  }),
64
65
  set: defineCommand({
@@ -107,7 +107,7 @@ and never go stale.
107
107
  - Use AI (`canopy.ai`): run `canopy ai models` before naming a model in code.
108
108
  Prefer the aliases `auto/low` (default) / `auto/mid` / `auto/high` over
109
109
  hard-coded ids — model ids outside that list are refused.
110
- - Work bug reports & feedback: `canopy feedback list | show | comment | status | report`
110
+ - Work bug reports & feedback: `canopy feedback list | show | comment | note | status | report | watch`
111
111
  — see [Working feedback reports](#working-feedback-reports-issues) below
112
112
  - CI credentials: `canopy token create | list | revoke`
113
113
  - Git: `canopy repo show --json` (is there a repository, and its remote) ·
@@ -127,15 +127,18 @@ short version:
127
127
  `canopy feedback show <id>` has the body, captured context, and comment
128
128
  trail. The context includes the deploy version that served the bug —
129
129
  reproduce against that, not whatever is live now. The id is enough on its
130
- own — `show`, `comment`, `status`, and `attach` work from any directory,
130
+ own — `show`, `comment`, `note`, `status`, and `attach` work from any directory,
131
131
  no `--project` needed.
132
132
  - Investigated ≠ fixed: write findings with
133
133
  `canopy feedback status <id> investigated --comment "<root cause>"`.
134
134
  - **`accepted` means a person said "build this."** Never set it on your own —
135
135
  only when the user you're working with says to build the thing.
136
136
  - Put tech-heavy notes (stack traces, table names, root-cause detail) in an
137
- internal comment (`canopy feedback comment <id> "…" --internal`) so they
138
- never reach the reporter.
137
+ internal note (`canopy feedback note <id> "…"`, same as `comment --internal`)
138
+ so they never reach the reporter.
139
+ - `canopy feedback watch [--project <slug>]… [--json]` follows new reports,
140
+ comments and status changes live; it reconnects by itself. With `--json`
141
+ each line carries an `id` cursor — restart with `--after <id>` to resume.
139
142
  - Resolve only once the fix is actually live, and name the version:
140
143
  `canopy feedback status <id> resolved --comment "fixed in v42"`. Attach
141
144
  proof when it's visual: `canopy feedback attach <id> ./after-fix.png`.
@@ -1,5 +1,5 @@
1
- import { t as version } from "./package-BY69Tjq_.mjs";
2
- import { r as reportErrors } from "./ui-CHfKV6lG.mjs";
1
+ import { t as version } from "./package-CJJpjVh_.mjs";
2
+ import { r as reportErrors } from "./ui-CaHgvvkJ.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import { cpSync, existsSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
5
5
  import { homedir } from "node:os";
@@ -1,6 +1,6 @@
1
1
  import { c as resolveProject, l as stagingUrl, o as projectFlagArgs, s as projectUrl } from "./site-DM2J10CB.mjs";
2
- import { i as apiRequest, o as withContext } from "./api-BPEYm9ts.mjs";
3
- import { a as unwrap, i as requireTty, r as reportErrors, t as formatDateTime } from "./ui-CHfKV6lG.mjs";
2
+ import { a as apiRequest, l as withContext } from "./api-ArfD6-t5.mjs";
3
+ import { a as unwrap, i as requireTty, r as reportErrors, t as formatDateTime } from "./ui-CaHgvvkJ.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  import * as p from "@clack/prompts";
6
6
  //#region src/staging.ts
@@ -1,6 +1,6 @@
1
1
  import { a as projectDirArgs, c as resolveProject, o as projectFlagArgs } from "./site-DM2J10CB.mjs";
2
- import { i as apiRequest } from "./api-BPEYm9ts.mjs";
3
- import { r as reportErrors, t as formatDateTime } from "./ui-CHfKV6lG.mjs";
2
+ import { a as apiRequest } from "./api-ArfD6-t5.mjs";
3
+ import { r as reportErrors, t as formatDateTime } from "./ui-CaHgvvkJ.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  const tokenCommand = defineCommand({
6
6
  meta: {
@@ -1,4 +1,4 @@
1
- import { t as ApiError } from "./api-BPEYm9ts.mjs";
1
+ import { t as ApiError } from "./api-ArfD6-t5.mjs";
2
2
  import { spawn } from "node:child_process";
3
3
  import { cancel, isCancel } from "@clack/prompts";
4
4
  //#region src/ui.ts
@@ -1,5 +1,5 @@
1
1
  import { t as PACKAGE_NAME } from "./version-check-CcsA1QjI.mjs";
2
- import { r as reportErrors } from "./ui-CHfKV6lG.mjs";
2
+ import { r as reportErrors } from "./ui-CaHgvvkJ.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-B96cKy8K.mjs");
115
+ const { skillInstalled } = await import("./skill-C2lGGz9n.mjs");
116
116
  const { homedir } = await import("node:os");
117
117
  const refreshGlobal = skillInstalled(homedir());
118
118
  const refreshRepo = process.cwd() !== homedir() && skillInstalled(process.cwd());
@@ -1,6 +1,6 @@
1
1
  import { c as resolveProject, o as projectFlagArgs } from "./site-DM2J10CB.mjs";
2
- import { i as apiRequest, o as withContext } from "./api-BPEYm9ts.mjs";
3
- import { r as reportErrors } from "./ui-CHfKV6lG.mjs";
2
+ import { a as apiRequest, l as withContext } from "./api-ArfD6-t5.mjs";
3
+ import { r as reportErrors } from "./ui-CaHgvvkJ.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  //#region src/usage.ts
6
6
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mediamonks/canopy-cli",
3
- "version": "0.7.37",
3
+ "version": "0.7.39",
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 { n as devCommand } from "./dev-BtnEuAnS.mjs";
2
- export { devCommand };
@@ -1,2 +0,0 @@
1
- import { i as repoCommand } from "./repo-CKIQrrHK.mjs";
2
- export { repoCommand };