@mediamonks/canopy-cli 0.7.5 → 0.7.7
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/docs/cli-create.md +46 -0
- package/dist/docs/cli-db.md +86 -0
- package/dist/docs/cli-delete.md +44 -0
- package/dist/docs/cli-deploy.md +58 -0
- package/dist/docs/cli-dev.md +63 -0
- package/dist/docs/cli-docs.md +47 -0
- package/dist/docs/cli-download.md +42 -0
- package/dist/docs/cli-egress.md +37 -0
- package/dist/docs/cli-feedback.md +51 -0
- package/dist/docs/cli-index.md +61 -0
- package/dist/docs/cli-init.md +46 -0
- package/dist/docs/cli-login.md +43 -0
- package/dist/docs/cli-logout.md +32 -0
- package/dist/docs/cli-logs.md +57 -0
- package/dist/docs/cli-open.md +41 -0
- package/dist/docs/cli-projects.md +43 -0
- package/dist/docs/cli-promote.md +44 -0
- package/dist/docs/cli-rollback.md +45 -0
- package/dist/docs/cli-skill.md +39 -0
- package/dist/docs/cli-staging.md +54 -0
- package/dist/docs/cli-token.md +47 -0
- package/dist/docs/cli-upgrade.md +43 -0
- package/dist/docs/cli-usage.md +43 -0
- package/dist/docs/cli-whoami.md +46 -0
- package/dist/docs/developers-agents-and-ai.md +95 -0
- package/dist/docs/developers-architecture.md +106 -0
- package/dist/docs/index.json +331 -0
- package/dist/docs/infrastructure.md +5 -8
- package/dist/docs/overview-faq.md +105 -0
- package/dist/docs/overview-how-it-works.md +81 -0
- package/dist/docs/overview-use-cases.md +76 -0
- package/dist/docs/overview-what-is-canopy.md +75 -0
- package/dist/docs/platform-ai-usage.md +65 -0
- package/dist/docs/platform-client-access.md +53 -0
- package/dist/docs/platform-deploys-and-rollbacks.md +93 -0
- package/dist/docs/platform-egress.md +75 -0
- package/dist/docs/platform-feedback.md +79 -0
- package/dist/docs/platform-members-and-access.md +71 -0
- package/dist/docs/platform-project-data.md +66 -0
- package/dist/docs/platform-schedules.md +90 -0
- package/dist/docs/platform-sites-and-profiles.md +61 -0
- package/dist/docs/static-serving.md +5 -8
- package/dist/docs/troubleshooting.md +6 -8
- package/dist/{docs-DQmUReJR.mjs → docs-DOiL2Sw-.mjs} +42 -20
- package/dist/feedback-BZG8E8zG.mjs +258 -0
- package/dist/index.mjs +10 -9
- package/dist/{logs-DBLDS3aQ.mjs → logs-DuzM-_Rn.mjs} +1 -1
- package/dist/{package-yVoO2pq4.mjs → package-BNiOibhj.mjs} +1 -1
- package/dist/{projects-D2fDeWNU.mjs → projects-C9TBoiW7.mjs} +5 -8
- package/dist/skill/canopy/SKILL.md +17 -8
- package/dist/{skill-12Ga-2UT.mjs → skill-DRTg-mJR.mjs} +1 -1
- package/dist/templates/worker/AGENTS.md +0 -3
- package/dist/templates/worker/README.md +1 -5
- package/dist/{upgrade-Du6U0xjp.mjs → upgrade-BDaM4M6Y.mjs} +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Schedules
|
|
3
|
+
description: Run named cron jobs in your project's backend — configured in the dashboard, so you can change the cadence without redeploying.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Schedules let a project do things on a clock: send a daily digest, refresh a
|
|
7
|
+
cache every hour, tick a counter every minute. You define **named jobs** in
|
|
8
|
+
the dashboard, and the platform wakes your project's backend at the right
|
|
9
|
+
times — your code only decides what each job does.
|
|
10
|
+
|
|
11
|
+
> **Note:** Schedules need a project with a custom backend. Per-project backends run on the **main** site and
|
|
12
|
+
> the **GCP pilot** site. On sites without backend support the Schedules section doesn't appear in
|
|
13
|
+
> Settings, and the API answers `not_supported_on_target`.
|
|
14
|
+
|
|
15
|
+
## Defining a schedule
|
|
16
|
+
|
|
17
|
+
In the dashboard, open the project's **Settings → Schedules**. Each schedule
|
|
18
|
+
has:
|
|
19
|
+
|
|
20
|
+
- a **name** (like `daily-digest`) — this is what your code switches on;
|
|
21
|
+
- a **cron expression** — standard 5-field cron, always in **UTC**;
|
|
22
|
+
- an **enabled** toggle, so you can pause a job without deleting it.
|
|
23
|
+
|
|
24
|
+
A project can have up to **5 schedules**, and the shortest interval is one
|
|
25
|
+
minute. Each row also shows its last run and, if it failed, the error — so
|
|
26
|
+
you can see at a glance whether a job is healthy.
|
|
27
|
+
|
|
28
|
+
You can also trigger a schedule manually from the dashboard, which is the
|
|
29
|
+
quickest way to test a handler without waiting for the clock.
|
|
30
|
+
|
|
31
|
+
## Handling schedules in your code
|
|
32
|
+
|
|
33
|
+
Your project's worker exports a `scheduled` handler next to its normal
|
|
34
|
+
`fetch` handler, and switches on the job's name:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
export default {
|
|
38
|
+
async fetch(request, env, ctx) {
|
|
39
|
+
return app.fetch(request, env, ctx);
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
async scheduled(event, env, ctx) {
|
|
43
|
+
switch (event.name) {
|
|
44
|
+
case "daily-digest":
|
|
45
|
+
await sendDigest(env);
|
|
46
|
+
break;
|
|
47
|
+
default:
|
|
48
|
+
console.warn("unknown schedule", event.name);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`event.name` is the name you gave the schedule in the dashboard, `event.cron`
|
|
55
|
+
is the expression that fired, and `event.scheduledTime` is when it fired.
|
|
56
|
+
Inside the handler your code has the same environment as an HTTP request —
|
|
57
|
+
secrets, the project database, and blob storage all work the same way.
|
|
58
|
+
|
|
59
|
+
## Data access without a signed-in user
|
|
60
|
+
|
|
61
|
+
A scheduled run has no browser session attached, but `canopy.db`, `canopy.blob`
|
|
62
|
+
and `canopy.ai` still work: the platform seals a **project-scoped worker
|
|
63
|
+
token** into your backend on every deploy, and calls through the platform
|
|
64
|
+
fall back to it whenever the request carries no user credential. You don't
|
|
65
|
+
configure or see this token — it's minted, rotated (every deploy replaces
|
|
66
|
+
it), and revoked with the project automatically, and it only ever reaches
|
|
67
|
+
your own project's data.
|
|
68
|
+
|
|
69
|
+
The one thing that behaves differently is identity: `canopy.auth.user()`
|
|
70
|
+
resolves with `kind: "worker"` and empty `id`/`email`/`name` — there is no
|
|
71
|
+
one signed in during a cron tick, and your code should treat that as "no
|
|
72
|
+
user", not as an error.
|
|
73
|
+
|
|
74
|
+
## Why the name matters
|
|
75
|
+
|
|
76
|
+
Switch on `event.name`, not on the cron string. The dashboard is the source
|
|
77
|
+
of truth for _when_ a job runs, so you can change `daily-digest` from 6:00 to
|
|
78
|
+
7:30 — or pause it — without touching code or redeploying. The name is the
|
|
79
|
+
stable contract between the dashboard and your handler.
|
|
80
|
+
|
|
81
|
+
## Things to know
|
|
82
|
+
|
|
83
|
+
- **Cron runs as the project, not as a person.** Data access works (see
|
|
84
|
+
above), but there is no signed-in user — `auth.user()` answers with
|
|
85
|
+
`kind: "worker"` and no identity.
|
|
86
|
+
- **All times are UTC.** `0 6 * * *` is 6:00 UTC, not local time.
|
|
87
|
+
- **Failures are visible.** A run that throws records its error on the
|
|
88
|
+
schedule row in Settings, so silent breakage doesn't stay silent.
|
|
89
|
+
|
|
90
|
+
- [Infrastructure](https://docs.canopy.website/guides/infrastructure) — What runs where — assets, backends, databases — across the Canopy sites.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Sites & profiles
|
|
3
|
+
description: Canopy runs as more than one installation. Most people only ever use main — here's what the others are and how the CLI keeps them straight.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Canopy isn't a single installation: the same platform runs as a few separate
|
|
7
|
+
**sites**, each with its own domain, its own projects, and its own data.
|
|
8
|
+
Unless someone told you otherwise, **you're on `main` and can stop reading
|
|
9
|
+
here** — the rest of this page explains what the other sites are for and how
|
|
10
|
+
the CLI targets them.
|
|
11
|
+
|
|
12
|
+
## The sites
|
|
13
|
+
|
|
14
|
+
| Site | Project URLs | What it's for |
|
|
15
|
+
| ----------- | --------------------- | ------------------------------------------------- |
|
|
16
|
+
| `main` | `*.canopy.website` | The real platform. Your projects live here. |
|
|
17
|
+
| `staging` | `*.thanarie.dev` | The platform team's testbed for platform changes. |
|
|
18
|
+
| `gcp-pilot` | `*.canopy.monks.zone` | A pilot running on Google Cloud. |
|
|
19
|
+
|
|
20
|
+
Sites are fully separate: a project on `main` doesn't exist on `staging`, and
|
|
21
|
+
signing in to one doesn't create anything on another. They can also differ in
|
|
22
|
+
what they support — the dashboard simply hides features a site can't honor,
|
|
23
|
+
and the API says `not_supported_on_target` if asked directly.
|
|
24
|
+
|
|
25
|
+
## What a profile is
|
|
26
|
+
|
|
27
|
+
A **profile** is the CLI's name for a site. `canopy login` signs you in once
|
|
28
|
+
via OneLogin and stores a credential for every site in one go; the profile
|
|
29
|
+
just picks which one a command talks to.
|
|
30
|
+
|
|
31
|
+
Three things decide the effective profile, in order:
|
|
32
|
+
|
|
33
|
+
1. `--profile <id>` on the command (or the `CANOPY_PROFILE` environment
|
|
34
|
+
variable) — one-off override.
|
|
35
|
+
2. The `"profile"` pinned in the project's `canopy.json` — set when the
|
|
36
|
+
project was created, so a checkout always targets the site it belongs to.
|
|
37
|
+
3. Your default active profile — `main` unless you chose otherwise at login.
|
|
38
|
+
|
|
39
|
+
Every command prints the site it's talking to in its header line, and
|
|
40
|
+
`canopy whoami` shows your email, active profile, and API host — so there's
|
|
41
|
+
never a mystery about where a deploy just went.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
canopy whoami # who am I, on which site?
|
|
45
|
+
canopy --profile gcp-pilot deploy # one command against the pilot
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## When you'd actually care
|
|
49
|
+
|
|
50
|
+
- **Never, usually.** Day-to-day work on your own projects is all `main`,
|
|
51
|
+
and the `canopy.json` pin keeps it that way without any flags.
|
|
52
|
+
- **Trying backend features.** Custom backends and
|
|
53
|
+
[schedules](https://docs.canopy.website/platform/schedules) currently run on `gcp-pilot`, so
|
|
54
|
+
experiments with those happen there.
|
|
55
|
+
- **Helping test platform changes.** If the platform team asks you to check
|
|
56
|
+
something on `staging`, `--profile staging` points any command at it.
|
|
57
|
+
|
|
58
|
+
> **Note:** Deploying to a different site publishes a **separate copy** of the project there — it doesn't move
|
|
59
|
+
> or mirror the one on `main`. Each site keeps its own versions, data, and members.
|
|
60
|
+
|
|
61
|
+
- [CLI reference](https://docs.canopy.website/guides/cli) — Every canopy command, including login, whoami, and the --profile flag.
|
|
@@ -7,12 +7,10 @@ This explains the full path a browser request takes to receive a deployed
|
|
|
7
7
|
project's `index.html` and its assets (JS/CSS/images/fonts), and reviews that
|
|
8
8
|
path from a **security** and a **performance** angle.
|
|
9
9
|
|
|
10
|
-
Everything a visitor sees is a _deployed_ build stored in R2
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
Cloudflare, per-project workers remain a Phase 2 TODO and the capability is
|
|
15
|
-
gated off.)
|
|
10
|
+
Everything a visitor sees is a _deployed_ build stored in R2 — the platform
|
|
11
|
+
serves pre-built static files. (When a project has a backend worker, `/api/*`
|
|
12
|
+
requests dispatch to it — Workers for Platforms on Cloudflare, Cloud Run on
|
|
13
|
+
GCP; everything else is still static serving.)
|
|
16
14
|
|
|
17
15
|
Asset serving is the hottest **data-plane** path (see
|
|
18
16
|
[`docs/09-plane-split.md`](../09-plane-split.md)): it lives at
|
|
@@ -213,8 +211,7 @@ These are observations, not known incidents — worth a conscious decision:
|
|
|
213
211
|
runs on a `*.canopy.<domain>` origin that shares the session-cookie domain.
|
|
214
212
|
The session cookie's flags (HttpOnly/SameSite/Secure) in
|
|
215
213
|
[`session.ts`](../../apps/gateway/src/session.ts) are the mitigation to keep an
|
|
216
|
-
eye on;
|
|
217
|
-
stronger fix.
|
|
214
|
+
eye on; per-project origin isolation would be the stronger fix.
|
|
218
215
|
3. **SPA fallback returns `index.html` with 200 for unknown paths**, including
|
|
219
216
|
things that look like missing assets (`/assets/typo.js` → HTML). That's normal
|
|
220
217
|
SPA behavior but can mask 404s and, combined with #1, is another reason
|
|
@@ -47,11 +47,11 @@ The `CANOPY_DEPLOY_TOKEN` was revoked or is wrong. Create a new one with
|
|
|
47
47
|
|
|
48
48
|
## `not_supported_on_target` (501)
|
|
49
49
|
|
|
50
|
-
Some features are platform-target-specific:
|
|
51
|
-
per-project worker backends, dashboard
|
|
52
|
-
|
|
53
|
-
failure of your project — the feature doesn't exist
|
|
54
|
-
runs on.
|
|
50
|
+
Some features are platform-target-specific: a site whose platform target
|
|
51
|
+
doesn't support a feature (for example per-project worker backends, dashboard
|
|
52
|
+
schedules, or secret injection) answers this error, and the dashboard hides
|
|
53
|
+
those panels. It's not a failure of your project — the feature doesn't exist
|
|
54
|
+
on the target your platform runs on.
|
|
55
55
|
|
|
56
56
|
## `ai_budget_exceeded` (429)
|
|
57
57
|
|
|
@@ -73,9 +73,7 @@ canopy egress allow api.example.com
|
|
|
73
73
|
|
|
74
74
|
or use dashboard Settings → Outbound hosts. Details and entry rules:
|
|
75
75
|
[Outbound requests](https://docs.canopy.website/platform/egress). Calls made through the SDK /
|
|
76
|
-
`env.CANOPY.fetch` don't count as egress and are unaffected.
|
|
77
|
-
target outbound requests are currently unrestricted, so this error only
|
|
78
|
-
appears on GCP.
|
|
76
|
+
`env.CANOPY.fetch` don't count as egress and are unaffected.
|
|
79
77
|
|
|
80
78
|
## The worker returns errors and you can't see why
|
|
81
79
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-BNiOibhj.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
4
|
import { n as reportErrors } from "./ui-CpdtK7mt.mjs";
|
|
@@ -9,9 +9,10 @@ import { join } from "node:path";
|
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
//#region src/docs.ts
|
|
11
11
|
/**
|
|
12
|
-
* canopy docs —
|
|
13
|
-
* (GET /control/v1/docs) so
|
|
14
|
-
* snapshot as offline
|
|
12
|
+
* canopy docs — the full docs site in the terminal (guides, platform, CLI and
|
|
13
|
+
* SDK references), served live by the platform (GET /control/v1/docs) so it
|
|
14
|
+
* never goes stale, with a local cache and a bundled snapshot as offline
|
|
15
|
+
* fallbacks. The listing is grouped per docs-site section.
|
|
15
16
|
*
|
|
16
17
|
* The platform requires a login for docs (internal platform), so fetches carry
|
|
17
18
|
* the CLI token when there is one; without it we simply fall back to the
|
|
@@ -98,15 +99,25 @@ function writeCache(file, content) {
|
|
|
98
99
|
writeFileSync(join(cacheDir, file), content);
|
|
99
100
|
} catch {}
|
|
100
101
|
}
|
|
101
|
-
/**
|
|
102
|
-
function
|
|
102
|
+
/** The index from cache, else from the bundled snapshot. */
|
|
103
|
+
function indexFromFallback() {
|
|
103
104
|
try {
|
|
104
105
|
const cached = JSON.parse(readFileSync(indexPath, "utf8"));
|
|
105
|
-
if (cached.topics?.length) return
|
|
106
|
+
if (cached.topics?.length) return {
|
|
107
|
+
topics: cached.topics,
|
|
108
|
+
sections: cached.sections
|
|
109
|
+
};
|
|
106
110
|
} catch {}
|
|
107
111
|
const dir = bundledDocsDir();
|
|
108
|
-
if (!dir) return [];
|
|
109
|
-
|
|
112
|
+
if (!dir) return { topics: [] };
|
|
113
|
+
try {
|
|
114
|
+
const { sections } = JSON.parse(readFileSync(join(dir, "index.json"), "utf8"));
|
|
115
|
+
if (sections?.length) return {
|
|
116
|
+
topics: sections.flatMap((s) => s.topics),
|
|
117
|
+
sections
|
|
118
|
+
};
|
|
119
|
+
} catch {}
|
|
120
|
+
return { topics: readdirSync(dir).filter((f) => /\.mdx?$/.test(f)).map((f) => {
|
|
110
121
|
const slug = f.replace(/\.mdx?$/, "");
|
|
111
122
|
const { title, summary } = parseFrontmatter(readFileSync(join(dir, f), "utf8"));
|
|
112
123
|
return {
|
|
@@ -114,7 +125,7 @@ function topicsFromFallback() {
|
|
|
114
125
|
title: title ?? slug,
|
|
115
126
|
summary: summary ?? ""
|
|
116
127
|
};
|
|
117
|
-
});
|
|
128
|
+
}) };
|
|
118
129
|
}
|
|
119
130
|
/** A single doc's body from cache, else from the bundled snapshot. */
|
|
120
131
|
function bodyFromFallback(slug) {
|
|
@@ -129,7 +140,7 @@ function bodyFromFallback(slug) {
|
|
|
129
140
|
}
|
|
130
141
|
}
|
|
131
142
|
}
|
|
132
|
-
async function
|
|
143
|
+
async function listIndex(refresh) {
|
|
133
144
|
const { apiUrl } = siteConfig();
|
|
134
145
|
if (refresh || !cacheFresh()) {
|
|
135
146
|
const data = await fetchJson(`${apiUrl}/control/v1/docs`);
|
|
@@ -138,10 +149,10 @@ async function listTopics(refresh) {
|
|
|
138
149
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
139
150
|
...data
|
|
140
151
|
}));
|
|
141
|
-
return data
|
|
152
|
+
return data;
|
|
142
153
|
}
|
|
143
154
|
}
|
|
144
|
-
return
|
|
155
|
+
return indexFromFallback();
|
|
145
156
|
}
|
|
146
157
|
async function readTopic(slug, refresh) {
|
|
147
158
|
const { apiUrl } = siteConfig();
|
|
@@ -157,7 +168,7 @@ async function readTopic(slug, refresh) {
|
|
|
157
168
|
const docsCommand = defineCommand({
|
|
158
169
|
meta: {
|
|
159
170
|
name: "docs",
|
|
160
|
-
description: "Read the Canopy
|
|
171
|
+
description: "Read the Canopy docs (offline-capable)"
|
|
161
172
|
},
|
|
162
173
|
args: {
|
|
163
174
|
topic: {
|
|
@@ -177,16 +188,27 @@ const docsCommand = defineCommand({
|
|
|
177
188
|
const latest = latestKnownVersion();
|
|
178
189
|
if (latest && compareSemver(version, latest) < 0) console.error(`Note: these docs describe CLI ${latest}; you have ${version} — commands or flags they mention may not exist here. Run \`canopy upgrade\`.\n`);
|
|
179
190
|
if (!topic) {
|
|
180
|
-
const
|
|
181
|
-
if (topics.length === 0) throw new Error("No docs available — check your connection, or reinstall the CLI.");
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
191
|
+
const index = await listIndex(refresh);
|
|
192
|
+
if (index.topics.length === 0) throw new Error("No docs available — check your connection, or reinstall the CLI.");
|
|
193
|
+
console.log("Canopy docs — run `canopy docs <topic>`:");
|
|
194
|
+
const groups = index.sections?.length ? index.sections : [{
|
|
195
|
+
key: "all",
|
|
196
|
+
title: "",
|
|
197
|
+
description: "",
|
|
198
|
+
topics: index.topics
|
|
199
|
+
}];
|
|
200
|
+
for (const section of groups) {
|
|
201
|
+
const heading = [section.title, section.description].filter(Boolean).join(" — ");
|
|
202
|
+
console.log(heading ? `\n${heading}` : "");
|
|
203
|
+
const width = Math.max(...section.topics.map((t) => t.slug.length));
|
|
204
|
+
for (const t of section.topics) console.log(` ${t.slug.padEnd(width)} ${t.summary}`);
|
|
205
|
+
}
|
|
185
206
|
return;
|
|
186
207
|
}
|
|
187
208
|
const body = await readTopic(topic, refresh);
|
|
188
209
|
if (body === void 0) {
|
|
189
|
-
const
|
|
210
|
+
const { topics } = await listIndex(false);
|
|
211
|
+
const known = topics.map((t) => t.slug).join(", ");
|
|
190
212
|
throw new Error(`No doc "${topic}".${known ? ` Available: ${known}.` : ""}`);
|
|
191
213
|
}
|
|
192
214
|
console.log(body);
|
|
@@ -0,0 +1,258 @@
|
|
|
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 };
|
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-BNiOibhj.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";
|
|
@@ -63,10 +63,10 @@ const main = defineCommand({
|
|
|
63
63
|
login: () => import("./login-C1x1l3qg.mjs").then((m) => m.loginCommand),
|
|
64
64
|
whoami: () => import("./login-C1x1l3qg.mjs").then((m) => m.whoamiCommand),
|
|
65
65
|
logout: () => import("./login-C1x1l3qg.mjs").then((m) => m.logoutCommand),
|
|
66
|
-
create: () => import("./projects-
|
|
67
|
-
init: () => import("./projects-
|
|
68
|
-
projects: () => import("./projects-
|
|
69
|
-
delete: () => import("./projects-
|
|
66
|
+
create: () => import("./projects-C9TBoiW7.mjs").then((m) => m.createCommand),
|
|
67
|
+
init: () => import("./projects-C9TBoiW7.mjs").then((m) => m.initCommand),
|
|
68
|
+
projects: () => import("./projects-C9TBoiW7.mjs").then((m) => m.projectsCommand),
|
|
69
|
+
delete: () => import("./projects-C9TBoiW7.mjs").then((m) => m.deleteCommand),
|
|
70
70
|
dev: () => import("./dev-D6Sy15Wv.mjs").then((m) => m.devCommand),
|
|
71
71
|
deploy: () => import("./deploy-ClioT9ZC.mjs").then((m) => m.deployCommand),
|
|
72
72
|
promote: () => import("./deploy-ClioT9ZC.mjs").then((m) => m.promoteCommand),
|
|
@@ -75,13 +75,14 @@ const main = defineCommand({
|
|
|
75
75
|
download: () => import("./deploy-ClioT9ZC.mjs").then((m) => m.downloadCommand),
|
|
76
76
|
token: () => import("./token-BjlqT2Xj.mjs").then((m) => m.tokenCommand),
|
|
77
77
|
egress: () => import("./egress-BkkG9Jzs.mjs").then((m) => m.egressCommand),
|
|
78
|
+
feedback: () => import("./feedback-BZG8E8zG.mjs").then((m) => m.feedbackCommand),
|
|
78
79
|
db: () => import("./db-D3RKaLVU.mjs").then((m) => m.dbCommand),
|
|
79
80
|
usage: () => import("./usage-wpew0a2M.mjs").then((m) => m.usageCommand),
|
|
80
81
|
open: () => import("./open-CLSh6oez.mjs").then((m) => m.openCommand),
|
|
81
|
-
docs: () => import("./docs-
|
|
82
|
-
skill: () => import("./skill-
|
|
83
|
-
upgrade: () => import("./upgrade-
|
|
84
|
-
logs: () => import("./logs-
|
|
82
|
+
docs: () => import("./docs-DOiL2Sw-.mjs").then((m) => m.docsCommand),
|
|
83
|
+
skill: () => import("./skill-DRTg-mJR.mjs").then((m) => m.skillCommand),
|
|
84
|
+
upgrade: () => import("./upgrade-BDaM4M6Y.mjs").then((m) => m.upgradeCommand),
|
|
85
|
+
logs: () => import("./logs-DuzM-_Rn.mjs").then((m) => m.logsCommand)
|
|
85
86
|
}
|
|
86
87
|
});
|
|
87
88
|
if (process.argv[2] !== "upgrade") notifyOfUpdate(version);
|
|
@@ -34,7 +34,7 @@ async function fetchLogs(slug, params) {
|
|
|
34
34
|
if (params.limit) query.set("limit", String(params.limit));
|
|
35
35
|
return withContext(apiRequest("GET", `/projects/${slug}/logs?${query}`), {
|
|
36
36
|
action: `read worker logs for "${slug}"`,
|
|
37
|
-
fixes: { not_supported_on_target: "Worker logs aren't available on this site yet
|
|
37
|
+
fixes: { not_supported_on_target: "Worker logs aren't available on this site yet." }
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
40
|
const logsCommand = defineCommand({
|