@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.
- package/dist/{db-D3RKaLVU.mjs → db-8Etjmzxh.mjs} +2 -2
- package/dist/{deploy-ClioT9ZC.mjs → deploy-y75cgauK.mjs} +2 -2
- package/dist/dev-CQq2uzSz.mjs +2 -0
- package/dist/{dev-BbAVZcAR.mjs → dev-CxfnHPI4.mjs} +4 -4
- package/dist/docs/cli-feedback.md +11 -3
- package/dist/docs/platform-feedback.md +22 -0
- package/dist/docs/platform-project-data.md +19 -9
- package/dist/{docs-DOiL2Sw-.mjs → docs-59NjixYd.mjs} +2 -2
- package/dist/{egress-BkkG9Jzs.mjs → egress-CN_axsAN.mjs} +1 -1
- package/dist/feedback-B7QOyMau.mjs +387 -0
- package/dist/index.mjs +24 -24
- package/dist/{local-BHxSZwUT.mjs → local-D-HcDDzL.mjs} +160 -6
- package/dist/{login-C1x1l3qg.mjs → login-Cxbj3F46.mjs} +1 -1
- package/dist/{logs-DuzM-_Rn.mjs → logs-DNiZquki.mjs} +1 -1
- package/dist/{migrations-DpWHuqkQ.mjs → migrations-C-5x6SYf.mjs} +1 -1
- package/dist/{open-CLSh6oez.mjs → open-DRPp4QJg.mjs} +1 -1
- package/dist/{package-BNiOibhj.mjs → package-27IYBxoU.mjs} +1 -1
- package/dist/{projects-C9TBoiW7.mjs → projects-C6JQjVqg.mjs} +1 -1
- package/dist/{skill-DRTg-mJR.mjs → skill-7K_P-5oN.mjs} +2 -2
- package/dist/{staging-95xeXyZN.mjs → staging-Bw1D_J8M.mjs} +1 -1
- package/dist/{token-BjlqT2Xj.mjs → token-D7zrIF8G.mjs} +1 -1
- package/dist/{ui-CpdtK7mt.mjs → ui-2mrJcjYC.mjs} +30 -1
- package/dist/{upgrade-BDaM4M6Y.mjs → upgrade-CqADJbUX.mjs} +2 -2
- package/dist/{usage-wpew0a2M.mjs → usage-CX1Izj_l.mjs} +1 -1
- package/package.json +1 -1
- package/dist/dev-D6Sy15Wv.mjs +0 -2
- 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,
|
|
3
|
-
import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-
|
|
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,
|
|
3
|
-
import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-
|
|
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";
|
|
@@ -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,
|
|
3
|
-
import { r as readMigrationFiles } from "./migrations-
|
|
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-
|
|
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-
|
|
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
|
|
32
|
-
version, console errors — or CLI context),
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
64
|
-
whoami: () => import("./login-
|
|
65
|
-
logout: () => import("./login-
|
|
66
|
-
create: () => import("./projects-
|
|
67
|
-
init: () => import("./projects-
|
|
68
|
-
projects: () => import("./projects-
|
|
69
|
-
delete: () => import("./projects-
|
|
70
|
-
dev: () => import("./dev-
|
|
71
|
-
deploy: () => import("./deploy-
|
|
72
|
-
promote: () => import("./deploy-
|
|
73
|
-
staging: () => import("./staging-
|
|
74
|
-
rollback: () => import("./deploy-
|
|
75
|
-
download: () => import("./deploy-
|
|
76
|
-
token: () => import("./token-
|
|
77
|
-
egress: () => import("./egress-
|
|
78
|
-
feedback: () => import("./feedback-
|
|
79
|
-
db: () => import("./db-
|
|
80
|
-
usage: () => import("./usage-
|
|
81
|
-
open: () => import("./open-
|
|
82
|
-
docs: () => import("./docs-
|
|
83
|
-
skill: () => import("./skill-
|
|
84
|
-
upgrade: () => import("./upgrade-
|
|
85
|
-
logs: () => import("./logs-
|
|
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-
|
|
2
|
-
import { n as rewriteApiPath } from "./dev-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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,
|
|
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-
|
|
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-
|
|
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,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-
|
|
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-
|
|
2
|
-
import { n as reportErrors } from "./ui-
|
|
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,
|
|
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-
|
|
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,
|
|
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-
|
|
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-
|
|
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,
|
|
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
package/dist/dev-D6Sy15Wv.mjs
DELETED
|
@@ -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 };
|