@mediamonks/canopy-cli 0.7.7 → 0.7.8

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 (27) hide show
  1. package/dist/{db-D3RKaLVU.mjs → db-8Etjmzxh.mjs} +2 -2
  2. package/dist/{deploy-ClioT9ZC.mjs → deploy-y75cgauK.mjs} +2 -2
  3. package/dist/dev-CQq2uzSz.mjs +2 -0
  4. package/dist/{dev-BbAVZcAR.mjs → dev-CxfnHPI4.mjs} +4 -4
  5. package/dist/docs/cli-feedback.md +11 -3
  6. package/dist/docs/platform-feedback.md +22 -0
  7. package/dist/docs/platform-project-data.md +19 -9
  8. package/dist/{docs-DOiL2Sw-.mjs → docs-59NjixYd.mjs} +2 -2
  9. package/dist/{egress-BkkG9Jzs.mjs → egress-CN_axsAN.mjs} +1 -1
  10. package/dist/feedback-B7QOyMau.mjs +387 -0
  11. package/dist/index.mjs +24 -24
  12. package/dist/{local-BHxSZwUT.mjs → local-D-HcDDzL.mjs} +160 -6
  13. package/dist/{login-C1x1l3qg.mjs → login-Cxbj3F46.mjs} +1 -1
  14. package/dist/{logs-DuzM-_Rn.mjs → logs-DNiZquki.mjs} +1 -1
  15. package/dist/{migrations-DpWHuqkQ.mjs → migrations-C-5x6SYf.mjs} +1 -1
  16. package/dist/{open-CLSh6oez.mjs → open-DRPp4QJg.mjs} +1 -1
  17. package/dist/{package-BNiOibhj.mjs → package-27IYBxoU.mjs} +1 -1
  18. package/dist/{projects-C9TBoiW7.mjs → projects-C6JQjVqg.mjs} +1 -1
  19. package/dist/{skill-DRTg-mJR.mjs → skill-7K_P-5oN.mjs} +2 -2
  20. package/dist/{staging-95xeXyZN.mjs → staging-Bw1D_J8M.mjs} +1 -1
  21. package/dist/{token-BjlqT2Xj.mjs → token-D7zrIF8G.mjs} +1 -1
  22. package/dist/{ui-CpdtK7mt.mjs → ui-2mrJcjYC.mjs} +30 -1
  23. package/dist/{upgrade-BDaM4M6Y.mjs → upgrade-CqADJbUX.mjs} +2 -2
  24. package/dist/{usage-wpew0a2M.mjs → usage-CX1Izj_l.mjs} +1 -1
  25. package/package.json +1 -1
  26. package/dist/dev-D6Sy15Wv.mjs +0 -2
  27. package/dist/feedback-BZG8E8zG.mjs +0 -258
@@ -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-CpdtK7mt.mjs";
3
- import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-DpWHuqkQ.mjs";
2
+ import { i as unwrap, n as reportErrors, o as apiDataRequest, r as requireTty, u as withContext } from "./ui-2mrJcjYC.mjs";
3
+ import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-C-5x6SYf.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-CpdtK7mt.mjs";
3
- import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-DpWHuqkQ.mjs";
2
+ import { c as apiRequest, i as unwrap, n as reportErrors, o as apiDataRequest, s as apiDownload, u as withContext } from "./ui-2mrJcjYC.mjs";
3
+ import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-C-5x6SYf.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";
@@ -0,0 +1,2 @@
1
+ import { t as devCommand } from "./dev-CxfnHPI4.mjs";
2
+ export { devCommand };
@@ -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-CpdtK7mt.mjs";
3
- import { r as readMigrationFiles } from "./migrations-DpWHuqkQ.mjs";
2
+ import { a as ApiError, c as apiRequest, n as reportErrors, u as withContext } from "./ui-2mrJcjYC.mjs";
3
+ import { r as readMigrationFiles } from "./migrations-C-5x6SYf.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-BHxSZwUT.mjs");
115
+ const { fetchProjectDump } = await import("./local-D-HcDDzL.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-BHxSZwUT.mjs");
130
+ const { createSandbox } = await import("./local-D-HcDDzL.mjs");
131
131
  const sandbox = await createSandbox({
132
132
  slug,
133
133
  persistDir: join(flags.dir, ".canopy", "local"),
@@ -16,6 +16,8 @@ canopy feedback comment <id> "root cause: the upload grant expires too early"
16
16
  canopy feedback status <id> resolved --comment "fixed in v42"
17
17
  canopy feedback mine
18
18
  canopy feedback report "canopy deploy exits 1 after upload" --command "canopy deploy"
19
+ canopy feedback report "the header overlaps the logo" --attach ./shot.png
20
+ canopy feedback attach <id> ./after-fix.png --comment "fixed — see the shot"
19
21
  ```
20
22
 
21
23
  ## Subcommands
@@ -28,14 +30,20 @@ machine-readable output.
28
30
  - `list [--status <s>] [--kind <k>]` — the project's inbox. Statuses:
29
31
  `new`, `accepted`, `in_progress`, `resolved`, `declined`; kinds: `bug`,
30
32
  `feedback`, `docs`.
31
- - `show <id>` — full detail: body, captured context (page URL, deploy
32
- version, console errors — or CLI context), and the comment trail.
33
+ - `show <id> [--download <dir>]` — full detail: body, captured context (page
34
+ URL, deploy version, console errors — or CLI context), the screenshots on
35
+ the report, and the comment trail. `--download` saves the screenshots into
36
+ a directory so you can actually look at them.
33
37
  - `comment <id> <text>` — append a comment (findings, questions, updates).
38
+ - `attach <id> <file> [--attach <more>] [--caption <text>] [--comment <text>]` —
39
+ add screenshots to an existing report. With `--comment`, the comment is
40
+ posted first and the screenshots hang off that trail entry — how you show
41
+ "here's what it looks like after the fix".
34
42
  - `status <id> <new-status> [--comment <note>]` — move the ticket; the
35
43
  transition is recorded on the trail.
36
44
  - `mine [--project <slug>] [--json]` — your own reports across all projects,
37
45
  with current statuses.
38
- - `report <text> [--kind bug|feedback] [--project <slug>] [--command <cmd>] [--error <output>]` —
46
+ - `report <text> [--kind bug|feedback] [--project <slug>] [--command <cmd>] [--error <output>] [--attach <file>] [--caption <text>]` —
39
47
  file a new report. **Without `--project` it goes to the platform inbox**
40
48
  (Canopy itself) — the right target when a `canopy` command misbehaves. The
41
49
  CLI attaches its version, the active site, your platform, and the project
@@ -29,6 +29,28 @@ boards.
29
29
  "Bug" for "Docs issue", and `canopy-feedback-prompt` replaces the textarea
30
30
  placeholder. Kinds are limited to `bug`, `feedback`, `docs`.
31
31
 
32
+ ## Screenshots
33
+
34
+ A screenshot is often the whole report, so you can attach up to six to any
35
+ report:
36
+
37
+ - **In the widget:** click **Attach screenshot**, or paste one straight from
38
+ the clipboard into the message box, or drop an image on the panel. Thumbnails
39
+ show what will be sent, and each one can be removed before you hit Send.
40
+ - **On the dashboard:** the same three ways, from the report itself — so you can
41
+ add a picture to a report after filing it, or to a comment as you write it
42
+ ("here's what it looks like after the fix"). Click any thumbnail for the full
43
+ image, then use the arrow keys to page through the rest.
44
+ - **From the CLI:** `canopy feedback report "…" --attach ./shot.png`, or
45
+ `canopy feedback attach <id> ./after-fix.png` for a report that already
46
+ exists. `canopy feedback show <id> --download <dir>` saves a report's
47
+ screenshots so an agent can look at them.
48
+
49
+ PNG, JPEG, WebP and GIF, up to 5 MB each. Large screenshots are scaled down in
50
+ your browser before they're sent, so a full-screen retina capture goes up small
51
+ without you doing anything. Screenshots are visible to the same people as the
52
+ report itself, and are deleted with it.
53
+
32
54
  ## The inbox
33
55
 
34
56
  Project members triage reports on the **Feedback tab**:
@@ -8,17 +8,27 @@ created automatically the moment the project exists. There are no connection
8
8
  strings to configure and no servers to run — your app reads and writes through
9
9
  the Canopy SDK, and no other project can ever see your data.
10
10
 
11
- ## The Data tab
11
+ ## The Data tab and the data explorer
12
12
 
13
13
  Open a project in the dashboard and the **Data** tab shows what your app has
14
- stored:
15
-
16
- - **Collections** — the document-style data your app writes with
17
- `canopy.db.collection(...)`. Pick a collection and page through its rows.
18
- - **Tables** — a browser for the underlying database tables, for when your
19
- app uses raw SQL or you want to see exactly what's on disk.
20
- - **SQL console** — run a query directly against the project's database from
21
- the browser. Useful for quick checks and one-off fixes.
14
+ stored — every collection and table at a glance — plus the door into the
15
+ **data explorer**, a full-page workspace for actually digging in:
16
+
17
+ - **Schema sidebar** — collections and tables, each expandable to its
18
+ columns (types, primary keys) and indexes.
19
+ - **Browse** — click any table or collection to page through it with real
20
+ totals, sort by any column, and stack filters (equals, contains,
21
+ comparisons, null checks) without writing SQL. Click a row for the full
22
+ record, JSON pretty-printed. Export the current page as CSV or JSON.
23
+ - **SQL console** — a real editor with syntax highlighting and autocomplete
24
+ fed by your schema. Run with <kbd>⌘⏎</kbd>, see row counts and timing,
25
+ recall earlier queries from history, and hit **Explain** to see the query
26
+ plan.
27
+
28
+ The console is **read-only by default** — the platform rejects anything but
29
+ reads until you flip the writes toggle, so exploring production data is safe
30
+ by design. The explorer is fixed to the channel (live or staging) you opened
31
+ it from; to look at the other one, go back to the Data tab and switch there.
22
32
 
23
33
  The **Files** tab next to it lists the project's blob storage — the uploads
24
34
  and files your app has saved.
@@ -1,7 +1,7 @@
1
- import { t as version } from "./package-BNiOibhj.mjs";
1
+ import { t as version } from "./package-27IYBxoU.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-CpdtK7mt.mjs";
4
+ import { n as reportErrors } from "./ui-2mrJcjYC.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";
@@ -1,5 +1,5 @@
1
1
  import { a as projectDirArgs, c as resolveProject } from "./site-BUlQR117.mjs";
2
- import { c as apiRequest, n as reportErrors } from "./ui-CpdtK7mt.mjs";
2
+ import { c as apiRequest, n as reportErrors } from "./ui-2mrJcjYC.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  //#region src/egress.ts
5
5
  /**
@@ -0,0 +1,387 @@
1
+ import { t as version } from "./package-27IYBxoU.mjs";
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-2mrJcjYC.mjs";
4
+ import { defineCommand } from "citty";
5
+ import { basename, extname, join } from "node:path";
6
+ import { readFile, writeFile } from "node:fs/promises";
7
+ //#region src/feedback.ts
8
+ /**
9
+ * canopy feedback — the ticket-lite feedback system (docs/19, docs topic
10
+ * `platform-feedback`). Two jobs:
11
+ *
12
+ * - Work a project's inbox: `list`, `show`, `comment`, `status` — the loop an
13
+ * agent runs over items a human marked `accepted`.
14
+ * - File reports: `report` sends a bug/feedback item. With no `--project` it
15
+ * lands on the **platform inbox** (canopy itself) — the right target when a
16
+ * canopy command misbehaves while you're building something else.
17
+ */
18
+ const STATUSES = [
19
+ "new",
20
+ "accepted",
21
+ "in_progress",
22
+ "resolved",
23
+ "declined"
24
+ ];
25
+ const KINDS = [
26
+ "bug",
27
+ "feedback",
28
+ "docs"
29
+ ];
30
+ /** Content-type hint from the extension; the server types the bytes itself. */
31
+ const IMAGE_TYPES = {
32
+ ".png": "image/png",
33
+ ".jpg": "image/jpeg",
34
+ ".jpeg": "image/jpeg",
35
+ ".webp": "image/webp",
36
+ ".gif": "image/gif"
37
+ };
38
+ /**
39
+ * Every `--attach` on the command line. citty 0.2.2 has no array arg type and
40
+ * keeps only the last occurrence, so repeats are read straight from argv;
41
+ * `--attach a.png,b.png` works as well, since agents write it both ways.
42
+ */
43
+ function attachedPaths(value) {
44
+ const flag = "--attach";
45
+ const repeated = process.argv.flatMap((arg, index) => {
46
+ if (arg === flag) {
47
+ const next = process.argv[index + 1];
48
+ return next && !next.startsWith("-") ? [next] : [];
49
+ }
50
+ return arg.startsWith(`${flag}=`) ? [arg.slice(9)] : [];
51
+ });
52
+ return (repeated.length > 0 ? repeated : value === void 0 ? [] : [String(value)]).flatMap((entry) => entry.split(",")).map((path) => path.trim()).filter((path) => path !== "");
53
+ }
54
+ /** Upload each file to a report that already exists (docs/21 create-then-attach). */
55
+ async function attachFiles(slug, itemId, paths, caption, commentId) {
56
+ for (const [index, path] of paths.entries()) {
57
+ const bytes = await readFile(path);
58
+ const params = new URLSearchParams({ filename: basename(path) });
59
+ if (caption) params.set("caption", paths.length > 1 ? `${caption} (${index + 1})` : caption);
60
+ if (commentId) params.set("comment_id", commentId);
61
+ await apiUpload(`/projects/${slug}/feedback/${encodeURIComponent(itemId)}/attachments?${params}`, bytes, IMAGE_TYPES[extname(path).toLowerCase()] ?? "application/octet-stream");
62
+ console.log(`Attached ${basename(path)}.`);
63
+ }
64
+ }
65
+ function sizeLabel(bytes) {
66
+ return bytes >= 1024 * 1024 ? `${(bytes / (1024 * 1024)).toFixed(1)} MB` : `${Math.max(1, Math.round(bytes / 1024))} KB`;
67
+ }
68
+ const jsonArg = { json: {
69
+ type: "boolean",
70
+ description: "Machine-readable JSON output"
71
+ } };
72
+ function firstLine(body) {
73
+ const line = body.split("\n")[0] ?? "";
74
+ return line.length > 72 ? `${line.slice(0, 71)}…` : line;
75
+ }
76
+ function printItems(items) {
77
+ if (items.length === 0) {
78
+ console.log("No matching feedback items.");
79
+ return;
80
+ }
81
+ for (const item of items) {
82
+ const comments = item.comment_count ? ` 💬${item.comment_count}` : "";
83
+ const shots = item.attachment_count ? ` 📎${item.attachment_count}` : "";
84
+ console.log(`${item.id} ${item.status} ${item.kind} ${firstLine(item.body)}${comments}${shots}`);
85
+ }
86
+ }
87
+ const listCommand = defineCommand({
88
+ meta: {
89
+ name: "list",
90
+ description: "List the project's feedback inbox (members only)"
91
+ },
92
+ args: {
93
+ ...projectDirArgs,
94
+ ...jsonArg,
95
+ status: {
96
+ type: "string",
97
+ description: `Filter: ${STATUSES.join(" | ")}`
98
+ },
99
+ kind: {
100
+ type: "string",
101
+ description: `Filter: ${KINDS.join(" | ")}`
102
+ }
103
+ },
104
+ run: reportErrors("feedback list", async ({ args }) => {
105
+ const { slug } = resolveProject(args);
106
+ const params = new URLSearchParams();
107
+ if (args.status) params.set("status", args.status);
108
+ if (args.kind) params.set("kind", args.kind);
109
+ const { items } = await apiRequest("GET", `/projects/${slug}/feedback${params.size > 0 ? `?${params}` : ""}`);
110
+ if (args.json) console.log(JSON.stringify(items, null, 2));
111
+ else printItems(items);
112
+ })
113
+ });
114
+ const showCommand = defineCommand({
115
+ meta: {
116
+ name: "show",
117
+ description: "Full detail of one item: body, context, comment trail"
118
+ },
119
+ args: {
120
+ id: {
121
+ type: "positional",
122
+ description: "Item id (from `canopy feedback list`)",
123
+ required: true
124
+ },
125
+ ...projectDirArgs,
126
+ ...jsonArg,
127
+ download: {
128
+ type: "string",
129
+ description: "Save the report's screenshots into this directory"
130
+ }
131
+ },
132
+ run: reportErrors("feedback show", async ({ args }) => {
133
+ const { slug } = resolveProject(args);
134
+ const detail = await apiRequest("GET", `/projects/${slug}/feedback/${encodeURIComponent(args.id)}`);
135
+ if (args.json) {
136
+ console.log(JSON.stringify(detail, null, 2));
137
+ return;
138
+ }
139
+ const { item, comments } = detail;
140
+ console.log(`${item.kind} · ${item.status} · via ${item.source} · ${item.created_at}`);
141
+ console.log(`reported by ${item.reporter_name || item.reporter_email || "unknown"}` + (item.deployment_id ? ` on ${item.deployment_id}` : "") + (item.channel === "staging" ? " (staging)" : ""));
142
+ console.log(`\n${item.body}\n`);
143
+ if (item.context) {
144
+ console.log("context:");
145
+ try {
146
+ console.log(JSON.stringify(JSON.parse(item.context), null, 2));
147
+ } catch {
148
+ console.log(item.context);
149
+ }
150
+ console.log("");
151
+ }
152
+ const shots = detail.attachments;
153
+ if (shots.length > 0) {
154
+ console.log("screenshots:");
155
+ for (const shot of shots) {
156
+ const label = shot.caption ? ` — ${shot.caption}` : "";
157
+ console.log(` ${shot.id} ${shot.filename ?? "screenshot"} ${sizeLabel(shot.size_bytes)}${label}`);
158
+ }
159
+ if (!args.download) console.log(" (save them with --download <dir> to look at them)");
160
+ console.log("");
161
+ }
162
+ for (const entry of comments) {
163
+ const author = entry.author_name || entry.author_email || "unknown";
164
+ const move = entry.status_to ? ` [${entry.status_from} → ${entry.status_to}]` : "";
165
+ console.log(`--- ${author} · ${entry.created_at}${move}`);
166
+ if (entry.body) console.log(entry.body);
167
+ }
168
+ if (args.download && shots.length > 0) {
169
+ console.log("");
170
+ for (const shot of shots) {
171
+ const bytes = await apiDownload(`/projects/${slug}/feedback/${encodeURIComponent(item.id)}/attachments/${shot.id}`);
172
+ const name = shot.filename ? `${shot.id}-${basename(shot.filename)}` : `${shot.id}.png`;
173
+ const target = join(args.download, name);
174
+ await writeFile(target, bytes);
175
+ console.log(`Saved ${target}`);
176
+ }
177
+ }
178
+ })
179
+ });
180
+ const commentCommand = defineCommand({
181
+ meta: {
182
+ name: "comment",
183
+ description: "Append a comment to an item (e.g. debug findings)"
184
+ },
185
+ args: {
186
+ id: {
187
+ type: "positional",
188
+ description: "Item id",
189
+ required: true
190
+ },
191
+ text: {
192
+ type: "positional",
193
+ description: "Comment text",
194
+ required: true
195
+ },
196
+ ...projectDirArgs
197
+ },
198
+ run: reportErrors("feedback comment", async ({ args }) => {
199
+ const { slug } = resolveProject(args);
200
+ await apiRequest("POST", `/projects/${slug}/feedback/${encodeURIComponent(args.id)}/comments`, { body: args.text });
201
+ console.log("Comment added.");
202
+ })
203
+ });
204
+ const statusCommand = defineCommand({
205
+ meta: {
206
+ name: "status",
207
+ description: "Move an item to a new status (members only)"
208
+ },
209
+ args: {
210
+ id: {
211
+ type: "positional",
212
+ description: "Item id",
213
+ required: true
214
+ },
215
+ to: {
216
+ type: "positional",
217
+ description: STATUSES.join(" | "),
218
+ required: true
219
+ },
220
+ comment: {
221
+ type: "string",
222
+ description: "Optional note recorded with the change"
223
+ },
224
+ ...projectDirArgs
225
+ },
226
+ run: reportErrors("feedback status", async ({ args }) => {
227
+ const { slug } = resolveProject(args);
228
+ await apiRequest("POST", `/projects/${slug}/feedback/${encodeURIComponent(args.id)}/status`, {
229
+ status: args.to,
230
+ ...args.comment ? { comment: args.comment } : {}
231
+ });
232
+ console.log(`Marked ${args.id} as ${args.to}.`);
233
+ })
234
+ });
235
+ const mineCommand = defineCommand({
236
+ meta: {
237
+ name: "mine",
238
+ description: "Your own reports across all projects"
239
+ },
240
+ args: {
241
+ ...jsonArg,
242
+ project: {
243
+ type: "string",
244
+ description: "Only reports on this project slug"
245
+ }
246
+ },
247
+ run: reportErrors("feedback mine", async ({ args }) => {
248
+ const { items } = await apiRequest("GET", `/feedback/mine${args.project ? `?project=${encodeURIComponent(args.project)}` : ""}`);
249
+ if (args.json) {
250
+ console.log(JSON.stringify(items, null, 2));
251
+ return;
252
+ }
253
+ if (items.length === 0) {
254
+ console.log("You haven't filed any reports.");
255
+ return;
256
+ }
257
+ for (const item of items) console.log(`${item.id} ${item.status} ${item.kind} [${item.project_slug}] ${firstLine(item.body)}`);
258
+ })
259
+ });
260
+ const reportCommand = defineCommand({
261
+ meta: {
262
+ name: "report",
263
+ description: "File a report. Default target is the platform inbox (canopy itself); use --project for a specific project"
264
+ },
265
+ args: {
266
+ text: {
267
+ type: "positional",
268
+ description: "What happened / what you suggest",
269
+ required: true
270
+ },
271
+ kind: {
272
+ type: "string",
273
+ description: "bug | feedback (default: bug)"
274
+ },
275
+ project: {
276
+ type: "string",
277
+ description: "Project slug to file against (default: the platform inbox)"
278
+ },
279
+ command: {
280
+ type: "string",
281
+ description: "The canopy command that failed (goes in context)"
282
+ },
283
+ error: {
284
+ type: "string",
285
+ description: "The error output you saw (goes in context)"
286
+ },
287
+ attach: {
288
+ type: "string",
289
+ description: "Screenshot to attach (PNG/JPEG/WebP/GIF); repeat for several"
290
+ },
291
+ caption: {
292
+ type: "string",
293
+ description: "One line describing the screenshot(s)"
294
+ },
295
+ status: {
296
+ type: "string",
297
+ description: `Initial status, project members only: ${STATUSES.join(" | ")}`
298
+ }
299
+ },
300
+ run: reportErrors("feedback report", async ({ args }) => {
301
+ const kind = args.kind ?? "bug";
302
+ if (kind !== "bug" && kind !== "feedback") {
303
+ console.error(`--kind must be bug or feedback (got "${kind}")`);
304
+ process.exit(1);
305
+ }
306
+ const context = {
307
+ cli_version: version,
308
+ site: siteConfig().profileId ?? siteConfig().apiUrl,
309
+ platform: `${process.platform} node ${process.versions.node}`
310
+ };
311
+ const workingOn = tryReadCanopyJson(".")?.slug;
312
+ if (workingOn) context["working_on"] = workingOn;
313
+ if (args.command) context["command"] = args.command;
314
+ if (args.error) context["error"] = args.error;
315
+ const paths = attachedPaths(args.attach);
316
+ for (const path of paths) await readFile(path);
317
+ const { item } = await apiRequest("POST", "/feedback", {
318
+ kind,
319
+ body: args.text,
320
+ ...args.project ? { project: args.project } : {},
321
+ ...args.status ? { status: args.status } : {},
322
+ context
323
+ });
324
+ console.log(`Filed ${kind} ${item.id} on ${item.project} as ${item.status}.`);
325
+ if (paths.length > 0) await attachFiles(item.project, item.id, paths, args.caption);
326
+ console.log(`Follow it with \`canopy feedback mine\` or on the dashboard's Reports page.`);
327
+ })
328
+ });
329
+ const feedbackCommand = defineCommand({
330
+ meta: {
331
+ name: "feedback",
332
+ description: "Work a project's feedback inbox, or file a report (see docs: platform-feedback)"
333
+ },
334
+ subCommands: {
335
+ list: listCommand,
336
+ show: showCommand,
337
+ comment: commentCommand,
338
+ attach: defineCommand({
339
+ meta: {
340
+ name: "attach",
341
+ description: "Attach screenshots to an existing item (e.g. proof that a fix works)"
342
+ },
343
+ args: {
344
+ id: {
345
+ type: "positional",
346
+ description: "Item id",
347
+ required: true
348
+ },
349
+ file: {
350
+ type: "positional",
351
+ description: "Image file (repeat --attach for more)",
352
+ required: true
353
+ },
354
+ attach: {
355
+ type: "string",
356
+ description: "Another image file; repeat for several"
357
+ },
358
+ caption: {
359
+ type: "string",
360
+ description: "One line describing the screenshot(s)"
361
+ },
362
+ comment: {
363
+ type: "string",
364
+ description: "Post this comment first and hang the screenshots off it"
365
+ },
366
+ ...projectDirArgs
367
+ },
368
+ run: reportErrors("feedback attach", async ({ args }) => {
369
+ const { slug } = resolveProject(args);
370
+ const paths = [args.file, ...attachedPaths(args.attach)];
371
+ for (const path of paths) await readFile(path);
372
+ let commentId;
373
+ if (args.comment) {
374
+ const { comment } = await apiRequest("POST", `/projects/${slug}/feedback/${encodeURIComponent(args.id)}/comments`, { body: args.comment });
375
+ commentId = comment.id;
376
+ console.log("Comment added.");
377
+ }
378
+ await attachFiles(slug, args.id, paths, args.caption, commentId);
379
+ })
380
+ }),
381
+ status: statusCommand,
382
+ mine: mineCommand,
383
+ report: reportCommand
384
+ }
385
+ });
386
+ //#endregion
387
+ export { feedbackCommand };
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as version } from "./package-BNiOibhj.mjs";
2
+ import { t as version } from "./package-27IYBxoU.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,29 +60,29 @@ const main = defineCommand({
60
60
  description: "canopy — create and deploy vibecoded projects"
61
61
  },
62
62
  subCommands: {
63
- login: () => import("./login-C1x1l3qg.mjs").then((m) => m.loginCommand),
64
- whoami: () => import("./login-C1x1l3qg.mjs").then((m) => m.whoamiCommand),
65
- logout: () => import("./login-C1x1l3qg.mjs").then((m) => m.logoutCommand),
66
- create: () => import("./projects-C9TBoiW7.mjs").then((m) => m.createCommand),
67
- init: () => import("./projects-C9TBoiW7.mjs").then((m) => m.initCommand),
68
- projects: () => import("./projects-C9TBoiW7.mjs").then((m) => m.projectsCommand),
69
- delete: () => import("./projects-C9TBoiW7.mjs").then((m) => m.deleteCommand),
70
- dev: () => import("./dev-D6Sy15Wv.mjs").then((m) => m.devCommand),
71
- deploy: () => import("./deploy-ClioT9ZC.mjs").then((m) => m.deployCommand),
72
- promote: () => import("./deploy-ClioT9ZC.mjs").then((m) => m.promoteCommand),
73
- staging: () => import("./staging-95xeXyZN.mjs").then((m) => m.stagingCommand),
74
- rollback: () => import("./deploy-ClioT9ZC.mjs").then((m) => m.rollbackCommand),
75
- download: () => import("./deploy-ClioT9ZC.mjs").then((m) => m.downloadCommand),
76
- token: () => import("./token-BjlqT2Xj.mjs").then((m) => m.tokenCommand),
77
- egress: () => import("./egress-BkkG9Jzs.mjs").then((m) => m.egressCommand),
78
- feedback: () => import("./feedback-BZG8E8zG.mjs").then((m) => m.feedbackCommand),
79
- db: () => import("./db-D3RKaLVU.mjs").then((m) => m.dbCommand),
80
- usage: () => import("./usage-wpew0a2M.mjs").then((m) => m.usageCommand),
81
- open: () => import("./open-CLSh6oez.mjs").then((m) => m.openCommand),
82
- docs: () => import("./docs-DOiL2Sw-.mjs").then((m) => m.docsCommand),
83
- skill: () => import("./skill-DRTg-mJR.mjs").then((m) => m.skillCommand),
84
- upgrade: () => import("./upgrade-BDaM4M6Y.mjs").then((m) => m.upgradeCommand),
85
- logs: () => import("./logs-DuzM-_Rn.mjs").then((m) => m.logsCommand)
63
+ login: () => import("./login-Cxbj3F46.mjs").then((m) => m.loginCommand),
64
+ whoami: () => import("./login-Cxbj3F46.mjs").then((m) => m.whoamiCommand),
65
+ logout: () => import("./login-Cxbj3F46.mjs").then((m) => m.logoutCommand),
66
+ create: () => import("./projects-C6JQjVqg.mjs").then((m) => m.createCommand),
67
+ init: () => import("./projects-C6JQjVqg.mjs").then((m) => m.initCommand),
68
+ projects: () => import("./projects-C6JQjVqg.mjs").then((m) => m.projectsCommand),
69
+ delete: () => import("./projects-C6JQjVqg.mjs").then((m) => m.deleteCommand),
70
+ dev: () => import("./dev-CQq2uzSz.mjs").then((m) => m.devCommand),
71
+ deploy: () => import("./deploy-y75cgauK.mjs").then((m) => m.deployCommand),
72
+ promote: () => import("./deploy-y75cgauK.mjs").then((m) => m.promoteCommand),
73
+ staging: () => import("./staging-Bw1D_J8M.mjs").then((m) => m.stagingCommand),
74
+ rollback: () => import("./deploy-y75cgauK.mjs").then((m) => m.rollbackCommand),
75
+ download: () => import("./deploy-y75cgauK.mjs").then((m) => m.downloadCommand),
76
+ token: () => import("./token-D7zrIF8G.mjs").then((m) => m.tokenCommand),
77
+ egress: () => import("./egress-CN_axsAN.mjs").then((m) => m.egressCommand),
78
+ feedback: () => import("./feedback-B7QOyMau.mjs").then((m) => m.feedbackCommand),
79
+ db: () => import("./db-8Etjmzxh.mjs").then((m) => m.dbCommand),
80
+ usage: () => import("./usage-CX1Izj_l.mjs").then((m) => m.usageCommand),
81
+ open: () => import("./open-DRPp4QJg.mjs").then((m) => m.openCommand),
82
+ docs: () => import("./docs-59NjixYd.mjs").then((m) => m.docsCommand),
83
+ skill: () => import("./skill-7K_P-5oN.mjs").then((m) => m.skillCommand),
84
+ upgrade: () => import("./upgrade-CqADJbUX.mjs").then((m) => m.upgradeCommand),
85
+ logs: () => import("./logs-DNiZquki.mjs").then((m) => m.logsCommand)
86
86
  }
87
87
  });
88
88
  if (process.argv[2] !== "upgrade") notifyOfUpdate(version);
@@ -1,5 +1,5 @@
1
- import { o as apiDataRequest } from "./ui-CpdtK7mt.mjs";
2
- import { n as rewriteApiPath } from "./dev-BbAVZcAR.mjs";
1
+ import { o as apiDataRequest } from "./ui-2mrJcjYC.mjs";
2
+ import { n as rewriteApiPath } from "./dev-CxfnHPI4.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";
@@ -126,6 +126,105 @@ const META_COLUMNS = {
126
126
  _updatedAt: "updated_at"
127
127
  };
128
128
  //#endregion
129
+ //#region ../data-plane/src/schema.ts
130
+ /** Platform/system tables never shown to users. */
131
+ const RESERVED = /^(_canopy_|_cf_|sqlite_|pg_)/;
132
+ /** Single-quoted SQL string literal (introspection args ride inline, not bound). */
133
+ const lit = (value) => `'${value.replaceAll("'", "''")}'`;
134
+ async function sqliteSchema(store) {
135
+ const names = (await store.query(`SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name`)).results.map((r) => String(r.name)).filter((n) => !RESERVED.test(n));
136
+ return Promise.all(names.map(async (name) => {
137
+ const [cols, idxList] = await Promise.all([store.query(`SELECT name, type, "notnull" AS not_null, pk FROM pragma_table_info(${lit(name)})`), store.query(`SELECT name, "unique" AS is_unique FROM pragma_index_list(${lit(name)})`)]);
138
+ const indexes = await Promise.all(idxList.results.filter((idx) => !RESERVED.test(String(idx.name))).map(async (idx) => ({
139
+ name: String(idx.name),
140
+ unique: Boolean(Number(idx.is_unique)),
141
+ columns: (await store.query(`SELECT name FROM pragma_index_info(${lit(String(idx.name))})`)).results.map((r) => String(r.name))
142
+ })));
143
+ return {
144
+ name,
145
+ columns: cols.results.map((col) => ({
146
+ name: String(col.name),
147
+ type: String(col.type),
148
+ nullable: !Number(col.not_null),
149
+ pk: Number(col.pk) > 0
150
+ })),
151
+ indexes
152
+ };
153
+ }));
154
+ }
155
+ async function postgresSchema(store) {
156
+ const [tables, columns, pks, indexes] = await Promise.all([
157
+ store.query(`SELECT table_name AS name FROM information_schema.tables
158
+ WHERE table_schema = current_schema() AND table_type = 'BASE TABLE'
159
+ ORDER BY table_name`),
160
+ store.query(`SELECT table_name, column_name, data_type, is_nullable
161
+ FROM information_schema.columns
162
+ WHERE table_schema = current_schema()
163
+ ORDER BY table_name, ordinal_position`),
164
+ store.query(`SELECT tc.table_name, kcu.column_name
165
+ FROM information_schema.table_constraints tc
166
+ JOIN information_schema.key_column_usage kcu
167
+ ON kcu.constraint_name = tc.constraint_name
168
+ AND kcu.table_schema = tc.table_schema
169
+ WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = current_schema()`),
170
+ store.query(`SELECT t.relname AS table_name, i.relname AS index_name,
171
+ ix.indisunique AS is_unique, a.attname AS column_name
172
+ FROM pg_index ix
173
+ JOIN pg_class i ON i.oid = ix.indexrelid
174
+ JOIN pg_class t ON t.oid = ix.indrelid
175
+ JOIN pg_namespace n ON n.oid = t.relnamespace
176
+ JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
177
+ WHERE n.nspname = current_schema() AND NOT ix.indisprimary`)
178
+ ]);
179
+ const pkSet = new Set(pks.results.map((r) => `${String(r.table_name)}.${String(r.column_name)}`));
180
+ const byTable = /* @__PURE__ */ new Map();
181
+ for (const row of tables.results) {
182
+ const name = String(row.name);
183
+ if (!RESERVED.test(name)) byTable.set(name, {
184
+ name,
185
+ columns: [],
186
+ indexes: []
187
+ });
188
+ }
189
+ for (const col of columns.results) {
190
+ const table = byTable.get(String(col.table_name));
191
+ if (!table) continue;
192
+ table.columns.push({
193
+ name: String(col.column_name),
194
+ type: String(col.data_type),
195
+ nullable: col.is_nullable === "YES",
196
+ pk: pkSet.has(`${String(col.table_name)}.${String(col.column_name)}`)
197
+ });
198
+ }
199
+ for (const idx of indexes.results) {
200
+ const table = byTable.get(String(idx.table_name));
201
+ if (!table) continue;
202
+ const name = String(idx.index_name);
203
+ let index = table.indexes.find((i) => i.name === name);
204
+ if (!index) {
205
+ index = {
206
+ name,
207
+ unique: Boolean(idx.is_unique),
208
+ columns: []
209
+ };
210
+ table.indexes.push(index);
211
+ }
212
+ index.columns.push(String(idx.column_name));
213
+ }
214
+ return [...byTable.values()];
215
+ }
216
+ async function introspectSchema(store, dialect) {
217
+ const [tables, collections] = await Promise.all([dialect === "postgres" ? postgresSchema(store) : sqliteSchema(store), store.query("SELECT collection, COUNT(*) AS docs FROM _canopy_docs GROUP BY collection ORDER BY collection").then((r) => r.results.map((row) => ({
218
+ name: String(row.collection),
219
+ docs: Number(row.docs)
220
+ }))).catch(() => [])]);
221
+ return {
222
+ dialect,
223
+ collections,
224
+ tables
225
+ };
226
+ }
227
+ //#endregion
129
228
  //#region ../data-plane/src/db.ts
130
229
  /**
131
230
  * /db — the SDK's document API + raw-SQL escape hatch, over an injectable
@@ -143,6 +242,29 @@ const COLLECTION_RE = /^[a-z][a-z0-9_]{0,63}$/;
143
242
  const FIELD_RE = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/;
144
243
  const MAX_DOC_BYTES = 256 * 1024;
145
244
  const MAX_LIMIT = 1e3;
245
+ /** Row cap for versioned /db/sql responses (the legacy shape is uncapped). */
246
+ const MAX_SQL_ROWS = 1e3;
247
+ /**
248
+ * Statement classifier for read-only /db/sql runs. First keyword must be a
249
+ * read; WITH additionally may not smuggle DML in a CTE (impossible on SQLite,
250
+ * possible on Postgres — where hosts also enforce `SET TRANSACTION READ ONLY`
251
+ * at the engine, making this the belt to that suspender).
252
+ */
253
+ function readOnlySqlViolation(query) {
254
+ const bare = query.replace(/--[^\n]*/g, " ").replace(/\/\*[\s\S]*?\*\//g, " ").replace(/'(?:[^']|'')*'/g, "''").replace(/"(?:[^"]|"")*"/g, "\"\"");
255
+ const first = /^\s*([A-Za-z]+)/.exec(bare)?.[1]?.toUpperCase();
256
+ if (first === void 0) return "empty statement";
257
+ if (![
258
+ "SELECT",
259
+ "EXPLAIN",
260
+ "VALUES",
261
+ "WITH"
262
+ ].includes(first)) return `${first} is not allowed in read-only mode`;
263
+ const dml = /\b(INSERT|UPDATE|DELETE|MERGE|REPLACE|CREATE|ALTER|DROP|TRUNCATE|GRANT|REVOKE|VACUUM|ATTACH|REINDEX)\b/i.exec(bare);
264
+ if (first === "WITH" && dml) return `${dml[1].toUpperCase()} is not allowed in read-only mode`;
265
+ if (first === "EXPLAIN" && dml && /\bANALYZE\b/i.test(bare)) return "EXPLAIN ANALYZE on a write is not allowed in read-only mode";
266
+ return null;
267
+ }
146
268
  function toDoc(row) {
147
269
  return {
148
270
  ...JSON.parse(row.data),
@@ -188,16 +310,48 @@ function dbRouter(config) {
188
310
  const { store } = resolved;
189
311
  const body = await c.req.json().catch(() => ({}));
190
312
  const params = body.params ?? [];
191
- if (typeof body.query !== "string" || body.query.trim() === "" || !Array.isArray(params) || !params.every(isScalar)) return c.json({
313
+ if (typeof body.query !== "string" || body.query.trim() === "" || !Array.isArray(params) || !params.every(isScalar) || !(body.readonly === void 0 || typeof body.readonly === "boolean")) return c.json({
192
314
  error: "invalid_query",
193
- detail: "expected { query: string, params?: (string|number|boolean|null)[] }"
315
+ detail: "expected { query: string, params?: (string|number|boolean|null)[], readonly?: boolean }"
194
316
  }, 400);
195
- const result = await store.query(body.query, params).catch((err) => err);
317
+ if (body.readonly === void 0) {
318
+ const result = await store.query(body.query, params).catch((err) => err);
319
+ if (result instanceof Error) return c.json({
320
+ error: "sql_error",
321
+ detail: result.message
322
+ }, 400);
323
+ return c.json(result.results);
324
+ }
325
+ if (body.readonly) {
326
+ const violation = readOnlySqlViolation(body.query);
327
+ if (violation) return c.json({
328
+ error: "read_only",
329
+ detail: violation
330
+ }, 400);
331
+ }
332
+ const result = await store.query(body.query, params, { readonly: body.readonly }).catch((err) => err);
196
333
  if (result instanceof Error) return c.json({
197
334
  error: "sql_error",
198
335
  detail: result.message
199
336
  }, 400);
200
- return c.json(result.results);
337
+ if (!body.readonly) await config.onSqlWrite?.(c, body.query);
338
+ const truncated = result.results.length > MAX_SQL_ROWS;
339
+ const rows = truncated ? result.results.slice(0, MAX_SQL_ROWS) : result.results;
340
+ return c.json({
341
+ rows,
342
+ rowCount: rows.length,
343
+ truncated
344
+ });
345
+ });
346
+ db.get("/db/schema", async (c) => {
347
+ const resolved = await resolve(c);
348
+ if (resolved instanceof Response) return resolved;
349
+ const schema = await introspectSchema(resolved.store, resolved.dialect.name).catch((err) => err);
350
+ if (schema instanceof Error) return c.json({
351
+ error: "sql_error",
352
+ detail: schema.message
353
+ }, 400);
354
+ return c.json(schema);
201
355
  });
202
356
  db.post("/db/:collection", async (c) => {
203
357
  const resolved = await resolve(c);
@@ -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-CpdtK7mt.mjs";
2
+ import { n as reportErrors, t as openBrowser } from "./ui-2mrJcjYC.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, l as withContext, n as reportErrors } from "./ui-CpdtK7mt.mjs";
2
+ import { c as apiRequest, n as reportErrors, u as withContext } from "./ui-2mrJcjYC.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-CpdtK7mt.mjs";
2
+ import { o as apiDataRequest } from "./ui-2mrJcjYC.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-CpdtK7mt.mjs";
2
+ import { n as reportErrors, t as openBrowser } from "./ui-2mrJcjYC.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.7";
2
+ var version = "0.7.8";
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-CpdtK7mt.mjs";
2
+ import { c as apiRequest, i as unwrap, n as reportErrors, r as requireTty } from "./ui-2mrJcjYC.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-BNiOibhj.mjs";
2
- import { n as reportErrors } from "./ui-CpdtK7mt.mjs";
1
+ import { t as version } from "./package-27IYBxoU.mjs";
2
+ import { n as reportErrors } from "./ui-2mrJcjYC.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-CpdtK7mt.mjs";
2
+ import { c as apiRequest, i as unwrap, n as reportErrors, r as requireTty, u as withContext } from "./ui-2mrJcjYC.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-CpdtK7mt.mjs";
2
+ import { c as apiRequest, n as reportErrors } from "./ui-2mrJcjYC.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  const tokenCommand = defineCommand({
5
5
  meta: {
@@ -102,6 +102,18 @@ const ERROR_COPY = {
102
102
  why: "the allowlist is at its 32-entry limit",
103
103
  fix: "Remove hosts you no longer need (`canopy egress list` / `canopy egress remove`), or cover several subdomains with one *. wildcard."
104
104
  },
105
+ unsupported_image: {
106
+ why: "the file isn't an image Canopy stores (PNG, JPEG, WebP or GIF)",
107
+ fix: "Attach a screenshot in one of those formats — the file's real contents are what count, not its extension."
108
+ },
109
+ attachment_too_large: {
110
+ why: "the screenshot is over the 5 MB limit",
111
+ fix: "Scale the image down (or export it as JPEG/WebP) and attach it again."
112
+ },
113
+ too_many_attachments: {
114
+ why: "the report already has the maximum number of screenshots",
115
+ fix: "Remove one from the report on the dashboard before attaching another."
116
+ },
105
117
  worker_egress_sync_failed: {
106
118
  why: "the list was saved but redeploying the running worker with it failed",
107
119
  fix: "Retry, or redeploy the project (`canopy deploy`) to apply the new allowlist."
@@ -211,6 +223,23 @@ async function apiRequest(method, path, body, rawContentType, headers) {
211
223
  if (!response.ok) return fail(response, isDeployToken, method, path);
212
224
  return await response.json();
213
225
  }
226
+ /**
227
+ * Authenticated POST of raw bytes (a screenshot attachment). The server types
228
+ * the bytes itself, so `contentType` is a hint, not a claim it trusts.
229
+ */
230
+ async function apiUpload(path, bytes, contentType) {
231
+ const { apiUrl, token, isDeployToken } = credentials();
232
+ const response = await fetch(`${apiUrl}/control/v1${path}`, {
233
+ method: "POST",
234
+ headers: {
235
+ authorization: `Bearer ${token}`,
236
+ "content-type": contentType
237
+ },
238
+ body: bytes
239
+ });
240
+ if (!response.ok) return fail(response, isDeployToken, "POST", path);
241
+ return await response.json();
242
+ }
214
243
  /** Authenticated GET returning raw bytes (e.g. a deploy zip). */
215
244
  async function apiDownload(path) {
216
245
  const { apiUrl, token, isDeployToken } = credentials();
@@ -346,4 +375,4 @@ function openBrowser(url) {
346
375
  }).on("error", () => {}).unref();
347
376
  }
348
377
  //#endregion
349
- export { ApiError as a, apiRequest as c, unwrap as i, withContext as l, reportErrors as n, apiDataRequest as o, requireTty as r, apiDownload as s, openBrowser as t };
378
+ 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 };
@@ -1,5 +1,5 @@
1
1
  import { t as PACKAGE_NAME } from "./version-check-CcsA1QjI.mjs";
2
- import { n as reportErrors } from "./ui-CpdtK7mt.mjs";
2
+ import { n as reportErrors } from "./ui-2mrJcjYC.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-DRTg-mJR.mjs");
115
+ const { skillInstalled } = await import("./skill-7K_P-5oN.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-CpdtK7mt.mjs";
2
+ import { c as apiRequest, n as reportErrors, u as withContext } from "./ui-2mrJcjYC.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.7",
3
+ "version": "0.7.8",
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-BbAVZcAR.mjs";
2
- export { devCommand };
@@ -1,258 +0,0 @@
1
- import { t as version } from "./package-BNiOibhj.mjs";
2
- import { a as projectDirArgs, c as resolveProject, n as siteConfig, u as tryReadCanopyJson } from "./site-BUlQR117.mjs";
3
- import { c as apiRequest, n as reportErrors } from "./ui-CpdtK7mt.mjs";
4
- import { defineCommand } from "citty";
5
- //#region src/feedback.ts
6
- /**
7
- * canopy feedback — the ticket-lite feedback system (docs/19, docs topic
8
- * `platform-feedback`). Two jobs:
9
- *
10
- * - Work a project's inbox: `list`, `show`, `comment`, `status` — the loop an
11
- * agent runs over items a human marked `accepted`.
12
- * - File reports: `report` sends a bug/feedback item. With no `--project` it
13
- * lands on the **platform inbox** (canopy itself) — the right target when a
14
- * canopy command misbehaves while you're building something else.
15
- */
16
- const STATUSES = [
17
- "new",
18
- "accepted",
19
- "in_progress",
20
- "resolved",
21
- "declined"
22
- ];
23
- const KINDS = [
24
- "bug",
25
- "feedback",
26
- "docs"
27
- ];
28
- const jsonArg = { json: {
29
- type: "boolean",
30
- description: "Machine-readable JSON output"
31
- } };
32
- function firstLine(body) {
33
- const line = body.split("\n")[0] ?? "";
34
- return line.length > 72 ? `${line.slice(0, 71)}…` : line;
35
- }
36
- function printItems(items) {
37
- if (items.length === 0) {
38
- console.log("No matching feedback items.");
39
- return;
40
- }
41
- for (const item of items) {
42
- const comments = item.comment_count ? ` 💬${item.comment_count}` : "";
43
- console.log(`${item.id} ${item.status} ${item.kind} ${firstLine(item.body)}${comments}`);
44
- }
45
- }
46
- const feedbackCommand = defineCommand({
47
- meta: {
48
- name: "feedback",
49
- description: "Work a project's feedback inbox, or file a report (see docs: platform-feedback)"
50
- },
51
- subCommands: {
52
- list: defineCommand({
53
- meta: {
54
- name: "list",
55
- description: "List the project's feedback inbox (members only)"
56
- },
57
- args: {
58
- ...projectDirArgs,
59
- ...jsonArg,
60
- status: {
61
- type: "string",
62
- description: `Filter: ${STATUSES.join(" | ")}`
63
- },
64
- kind: {
65
- type: "string",
66
- description: `Filter: ${KINDS.join(" | ")}`
67
- }
68
- },
69
- run: reportErrors("feedback list", async ({ args }) => {
70
- const { slug } = resolveProject(args);
71
- const params = new URLSearchParams();
72
- if (args.status) params.set("status", args.status);
73
- if (args.kind) params.set("kind", args.kind);
74
- const { items } = await apiRequest("GET", `/projects/${slug}/feedback${params.size > 0 ? `?${params}` : ""}`);
75
- if (args.json) console.log(JSON.stringify(items, null, 2));
76
- else printItems(items);
77
- })
78
- }),
79
- show: defineCommand({
80
- meta: {
81
- name: "show",
82
- description: "Full detail of one item: body, context, comment trail"
83
- },
84
- args: {
85
- id: {
86
- type: "positional",
87
- description: "Item id (from `canopy feedback list`)",
88
- required: true
89
- },
90
- ...projectDirArgs,
91
- ...jsonArg
92
- },
93
- run: reportErrors("feedback show", async ({ args }) => {
94
- const { slug } = resolveProject(args);
95
- const detail = await apiRequest("GET", `/projects/${slug}/feedback/${encodeURIComponent(args.id)}`);
96
- if (args.json) {
97
- console.log(JSON.stringify(detail, null, 2));
98
- return;
99
- }
100
- const { item, comments } = detail;
101
- console.log(`${item.kind} · ${item.status} · via ${item.source} · ${item.created_at}`);
102
- console.log(`reported by ${item.reporter_name || item.reporter_email || "unknown"}` + (item.deployment_id ? ` on ${item.deployment_id}` : "") + (item.channel === "staging" ? " (staging)" : ""));
103
- console.log(`\n${item.body}\n`);
104
- if (item.context) {
105
- console.log("context:");
106
- try {
107
- console.log(JSON.stringify(JSON.parse(item.context), null, 2));
108
- } catch {
109
- console.log(item.context);
110
- }
111
- console.log("");
112
- }
113
- for (const entry of comments) {
114
- const author = entry.author_name || entry.author_email || "unknown";
115
- const move = entry.status_to ? ` [${entry.status_from} → ${entry.status_to}]` : "";
116
- console.log(`--- ${author} · ${entry.created_at}${move}`);
117
- if (entry.body) console.log(entry.body);
118
- }
119
- })
120
- }),
121
- comment: defineCommand({
122
- meta: {
123
- name: "comment",
124
- description: "Append a comment to an item (e.g. debug findings)"
125
- },
126
- args: {
127
- id: {
128
- type: "positional",
129
- description: "Item id",
130
- required: true
131
- },
132
- text: {
133
- type: "positional",
134
- description: "Comment text",
135
- required: true
136
- },
137
- ...projectDirArgs
138
- },
139
- run: reportErrors("feedback comment", async ({ args }) => {
140
- const { slug } = resolveProject(args);
141
- await apiRequest("POST", `/projects/${slug}/feedback/${encodeURIComponent(args.id)}/comments`, { body: args.text });
142
- console.log("Comment added.");
143
- })
144
- }),
145
- status: defineCommand({
146
- meta: {
147
- name: "status",
148
- description: "Move an item to a new status (members only)"
149
- },
150
- args: {
151
- id: {
152
- type: "positional",
153
- description: "Item id",
154
- required: true
155
- },
156
- to: {
157
- type: "positional",
158
- description: STATUSES.join(" | "),
159
- required: true
160
- },
161
- comment: {
162
- type: "string",
163
- description: "Optional note recorded with the change"
164
- },
165
- ...projectDirArgs
166
- },
167
- run: reportErrors("feedback status", async ({ args }) => {
168
- const { slug } = resolveProject(args);
169
- await apiRequest("POST", `/projects/${slug}/feedback/${encodeURIComponent(args.id)}/status`, {
170
- status: args.to,
171
- ...args.comment ? { comment: args.comment } : {}
172
- });
173
- console.log(`Marked ${args.id} as ${args.to}.`);
174
- })
175
- }),
176
- mine: defineCommand({
177
- meta: {
178
- name: "mine",
179
- description: "Your own reports across all projects"
180
- },
181
- args: {
182
- ...jsonArg,
183
- project: {
184
- type: "string",
185
- description: "Only reports on this project slug"
186
- }
187
- },
188
- run: reportErrors("feedback mine", async ({ args }) => {
189
- const { items } = await apiRequest("GET", `/feedback/mine${args.project ? `?project=${encodeURIComponent(args.project)}` : ""}`);
190
- if (args.json) {
191
- console.log(JSON.stringify(items, null, 2));
192
- return;
193
- }
194
- if (items.length === 0) {
195
- console.log("You haven't filed any reports.");
196
- return;
197
- }
198
- for (const item of items) console.log(`${item.id} ${item.status} ${item.kind} [${item.project_slug}] ${firstLine(item.body)}`);
199
- })
200
- }),
201
- report: defineCommand({
202
- meta: {
203
- name: "report",
204
- description: "File a report. Default target is the platform inbox (canopy itself); use --project for a specific project"
205
- },
206
- args: {
207
- text: {
208
- type: "positional",
209
- description: "What happened / what you suggest",
210
- required: true
211
- },
212
- kind: {
213
- type: "string",
214
- description: "bug | feedback (default: bug)"
215
- },
216
- project: {
217
- type: "string",
218
- description: "Project slug to file against (default: the platform inbox)"
219
- },
220
- command: {
221
- type: "string",
222
- description: "The canopy command that failed (goes in context)"
223
- },
224
- error: {
225
- type: "string",
226
- description: "The error output you saw (goes in context)"
227
- }
228
- },
229
- run: reportErrors("feedback report", async ({ args }) => {
230
- const kind = args.kind ?? "bug";
231
- if (kind !== "bug" && kind !== "feedback") {
232
- console.error(`--kind must be bug or feedback (got "${kind}")`);
233
- process.exit(1);
234
- }
235
- const context = {
236
- cli_version: version,
237
- site: siteConfig().profileId ?? siteConfig().apiUrl,
238
- platform: `${process.platform} node ${process.versions.node}`
239
- };
240
- const workingOn = tryReadCanopyJson(".")?.slug;
241
- if (workingOn) context["working_on"] = workingOn;
242
- if (args.command) context["command"] = args.command;
243
- if (args.error) context["error"] = args.error;
244
- const { item } = await apiRequest("POST", "/feedback", {
245
- kind,
246
- body: args.text,
247
- source: "cli",
248
- ...args.project ? { project: args.project } : {},
249
- context
250
- });
251
- console.log(`Filed ${kind} ${item.id} on ${item.project}.`);
252
- console.log(`Follow it with \`canopy feedback mine\` or on the dashboard's Reports page.`);
253
- })
254
- })
255
- }
256
- });
257
- //#endregion
258
- export { feedbackCommand };