@mediamonks/canopy-cli 0.7.6 → 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 +59 -0
- package/dist/docs/index.json +10 -0
- package/dist/docs/platform-feedback.md +101 -0
- package/dist/docs/platform-project-data.md +19 -9
- package/dist/{docs-DhAz6QWS.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 -23
- 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-CPXtNnfc.mjs → package-27IYBxoU.mjs} +1 -1
- package/dist/{projects-C9TBoiW7.mjs → projects-C6JQjVqg.mjs} +1 -1
- package/dist/skill/canopy/SKILL.md +7 -0
- package/dist/{skill-BCX4cUeP.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-jLu7jI0r.mjs → upgrade-CqADJbUX.mjs} +2 -2
- package/dist/{usage-wpew0a2M.mjs → usage-CX1Izj_l.mjs} +1 -1
- package/package.json +3 -3
- package/dist/dev-D6Sy15Wv.mjs +0 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { c as resolveProject, o as projectFlagArgs } from "./site-BUlQR117.mjs";
|
|
2
|
-
import { i as unwrap,
|
|
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"),
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: canopy feedback
|
|
3
|
+
description: Read and triage a project's feedback inbox, or file a bug/feedback report — against a project or against Canopy itself.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`canopy feedback` is the CLI side of [Feedback & bug reports](https://docs.canopy.website/platform/feedback):
|
|
7
|
+
agents (and humans) pull reports, append findings, move statuses, and file new
|
|
8
|
+
ones.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
canopy feedback list --status accepted
|
|
14
|
+
canopy feedback show <id>
|
|
15
|
+
canopy feedback comment <id> "root cause: the upload grant expires too early"
|
|
16
|
+
canopy feedback status <id> resolved --comment "fixed in v42"
|
|
17
|
+
canopy feedback mine
|
|
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"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Subcommands
|
|
24
|
+
|
|
25
|
+
Inbox commands (`list`, `show`, `comment`, `status`) resolve the project the
|
|
26
|
+
usual way — `canopy.json` in the target directory, or `--project <slug>` — and
|
|
27
|
+
require project membership. `list` and `show` take `--json` for
|
|
28
|
+
machine-readable output.
|
|
29
|
+
|
|
30
|
+
- `list [--status <s>] [--kind <k>]` — the project's inbox. Statuses:
|
|
31
|
+
`new`, `accepted`, `in_progress`, `resolved`, `declined`; kinds: `bug`,
|
|
32
|
+
`feedback`, `docs`.
|
|
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.
|
|
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".
|
|
42
|
+
- `status <id> <new-status> [--comment <note>]` — move the ticket; the
|
|
43
|
+
transition is recorded on the trail.
|
|
44
|
+
- `mine [--project <slug>] [--json]` — your own reports across all projects,
|
|
45
|
+
with current statuses.
|
|
46
|
+
- `report <text> [--kind bug|feedback] [--project <slug>] [--command <cmd>] [--error <output>] [--attach <file>] [--caption <text>]` —
|
|
47
|
+
file a new report. **Without `--project` it goes to the platform inbox**
|
|
48
|
+
(Canopy itself) — the right target when a `canopy` command misbehaves. The
|
|
49
|
+
CLI attaches its version, the active site, your platform, and the project
|
|
50
|
+
you were working in.
|
|
51
|
+
|
|
52
|
+
## For agents
|
|
53
|
+
|
|
54
|
+
The intended loop: a human marks items `accepted` on the dashboard; the agent
|
|
55
|
+
runs `list --status accepted`, reproduces using the context in `show`
|
|
56
|
+
(including the deploy version that served the page), comments findings, and
|
|
57
|
+
resolves — or declines with a comment saying why. When a `canopy` command
|
|
58
|
+
itself fails: rule out user error, check `canopy feedback mine` for a
|
|
59
|
+
duplicate, then `report` with the exact command and error output.
|
package/dist/docs/index.json
CHANGED
|
@@ -129,6 +129,11 @@
|
|
|
129
129
|
"title": "Members & access",
|
|
130
130
|
"summary": "Everything on Canopy sits behind Monks sign-in. Collaborators manage a project; other Monks can view the live app — unless you tighten it."
|
|
131
131
|
},
|
|
132
|
+
{
|
|
133
|
+
"slug": "platform-feedback",
|
|
134
|
+
"title": "Feedback & bug reports",
|
|
135
|
+
"summary": "A built-in feedback widget on every project, a ticket-lite inbox behind it, and a CLI that lets agents work the backlog."
|
|
136
|
+
},
|
|
132
137
|
{
|
|
133
138
|
"slug": "platform-client-access",
|
|
134
139
|
"title": "Client access",
|
|
@@ -241,6 +246,11 @@
|
|
|
241
246
|
"title": "canopy usage",
|
|
242
247
|
"summary": "Show a project's AI spend, storage footprint, and deploy count from the terminal."
|
|
243
248
|
},
|
|
249
|
+
{
|
|
250
|
+
"slug": "cli-feedback",
|
|
251
|
+
"title": "canopy feedback",
|
|
252
|
+
"summary": "Read and triage a project's feedback inbox, or file a bug/feedback report — against a project or against Canopy itself."
|
|
253
|
+
},
|
|
244
254
|
{
|
|
245
255
|
"slug": "cli-open",
|
|
246
256
|
"title": "canopy open",
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Feedback & bug reports
|
|
3
|
+
description: A built-in feedback widget on every project, a ticket-lite inbox behind it, and a CLI that lets agents work the backlog.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Every Canopy project gets an in-app feedback channel without writing a line of
|
|
7
|
+
code: signed-in Monks see a small floating button on the live app, pick **Bug**
|
|
8
|
+
or **Feedback**, type what happened, and send. The report lands in the
|
|
9
|
+
project's **Feedback tab** on the dashboard, with everything an investigator
|
|
10
|
+
needs captured automatically — page URL, the deploy version that served it,
|
|
11
|
+
viewport, and recent console errors.
|
|
12
|
+
|
|
13
|
+
It is deliberately **not** a ticketing product: an item has a status, an
|
|
14
|
+
append-only comment trail, and nothing else. No assignees, priorities, or
|
|
15
|
+
boards.
|
|
16
|
+
|
|
17
|
+
## The widget
|
|
18
|
+
|
|
19
|
+
- Appears only for **signed-in Monks**. Client-password visitors never see it —
|
|
20
|
+
the platform doesn't even send them the script.
|
|
21
|
+
- On by default for every project. Turn it off per project in
|
|
22
|
+
**Settings → Feedback widget**; the change applies on the next page load.
|
|
23
|
+
- Submissions are always identified (your company sign-in), so there's no
|
|
24
|
+
anonymous spam and no reporter field to fill in.
|
|
25
|
+
- The widget also shows your own previous reports for that project with their
|
|
26
|
+
current status.
|
|
27
|
+
- A page can reshape the chooser with meta tags — this docs site does exactly
|
|
28
|
+
that: `<meta name="canopy-feedback-kinds" content="docs,feedback">` swaps
|
|
29
|
+
"Bug" for "Docs issue", and `canopy-feedback-prompt` replaces the textarea
|
|
30
|
+
placeholder. Kinds are limited to `bug`, `feedback`, `docs`.
|
|
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
|
+
|
|
54
|
+
## The inbox
|
|
55
|
+
|
|
56
|
+
Project members triage reports on the **Feedback tab**:
|
|
57
|
+
|
|
58
|
+
- Statuses: `new → accepted → in_progress → resolved`, plus `declined`.
|
|
59
|
+
**`accepted` means "this is valid — an agent may pick it up."**
|
|
60
|
+
- Every status change and comment is recorded on the item's trail.
|
|
61
|
+
- Reporters follow their submissions on the **Reports page** (dashboard
|
|
62
|
+
header) and can comment on or withdraw their own items; only project
|
|
63
|
+
members change statuses.
|
|
64
|
+
|
|
65
|
+
## Working the backlog with an agent
|
|
66
|
+
|
|
67
|
+
Because the CLI exposes the inbox, "work through the accepted feedback" is a
|
|
68
|
+
prompt, not a process:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
canopy feedback list --status accepted # what a human marked valid
|
|
72
|
+
canopy feedback show <id> # body + captured context + trail
|
|
73
|
+
canopy feedback comment <id> "root cause: …" # append findings
|
|
74
|
+
canopy feedback status <id> resolved --comment "fixed in v42"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`show` includes the deploy version that served the page when the bug was
|
|
78
|
+
reported — reproduce against that, not whatever is live now. An agent that
|
|
79
|
+
investigates but can't fix should `comment` its findings and leave the status
|
|
80
|
+
for a human; one that pushes back should `status <id> declined` with a comment
|
|
81
|
+
saying why.
|
|
82
|
+
|
|
83
|
+
## Reporting problems with Canopy itself
|
|
84
|
+
|
|
85
|
+
When a `canopy` command fails while you (or your agent) are building a
|
|
86
|
+
project, file it against the **platform inbox** — not your own project:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
canopy feedback report "deploy failed after upload" \
|
|
90
|
+
--command "canopy deploy --staging" --error "upload_grant_expired …"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
With no `--project`, the report goes to the platform team automatically, with
|
|
94
|
+
your CLI version, site, and the project you were working on attached. Before
|
|
95
|
+
filing: rule out user error first (`canopy docs troubleshooting`,
|
|
96
|
+
`vp env doctor` for toolchain issues), and skim `canopy feedback mine` to
|
|
97
|
+
avoid filing the same issue twice.
|
|
98
|
+
|
|
99
|
+
## CLI reference
|
|
100
|
+
|
|
101
|
+
See [canopy feedback](https://docs.canopy.website/cli/feedback) for every subcommand and flag.
|
|
@@ -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,28 +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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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)
|
|
85
86
|
}
|
|
86
87
|
});
|
|
87
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";
|
|
@@ -96,6 +96,8 @@ and never go stale.
|
|
|
96
96
|
- Roll back: `canopy rollback`
|
|
97
97
|
- Open the live site: `canopy open`
|
|
98
98
|
- Manage data: `canopy db pull | snapshot | snapshots | restore | migrate`
|
|
99
|
+
- Work bug reports & feedback: `canopy feedback list | show | comment | status | report`
|
|
100
|
+
— see `canopy docs platform-feedback` for the triage and reporting loops
|
|
99
101
|
- CI credentials: `canopy token create | list | revoke`
|
|
100
102
|
|
|
101
103
|
## Custom tables & migrations (devs; Drizzle recommended)
|
|
@@ -151,3 +153,8 @@ Non-interactive: `canopy create "My App" --template spa`. **When unsure between
|
|
|
151
153
|
- **Deploys are public.** `canopy deploy` makes a version live. Confirm before
|
|
152
154
|
deploying unless the user clearly asked you to.
|
|
153
155
|
- Keep the CLI current: `canopy upgrade` (this also refreshes this skill).
|
|
156
|
+
- **A `canopy` command itself failing?** First rule out user error (re-read
|
|
157
|
+
`canopy docs troubleshooting` and the command's `--help`). If the platform
|
|
158
|
+
really misbehaved, check `canopy feedback mine` for a duplicate, then file it:
|
|
159
|
+
`canopy feedback report "<what happened>" --command "<the command>" --error "<output>"`
|
|
160
|
+
— it reaches the platform team directly (no `--project` needed).
|
|
@@ -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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mediamonks/canopy-cli",
|
|
3
|
-
"version": "0.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"
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"esbuild": "^0.28.1",
|
|
21
21
|
"hono": "^4.8.0",
|
|
22
22
|
"vite-server": "npm:vite@^7",
|
|
23
|
-
"@mediamonks/canopy
|
|
24
|
-
"@mediamonks/canopy": "0.0.
|
|
23
|
+
"@mediamonks/canopy": "0.0.4",
|
|
24
|
+
"@mediamonks/canopy-data-plane": "0.0.2"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^26.1.1",
|
package/dist/dev-D6Sy15Wv.mjs
DELETED