@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.
Files changed (55) hide show
  1. package/dist/docs/cli-create.md +46 -0
  2. package/dist/docs/cli-db.md +86 -0
  3. package/dist/docs/cli-delete.md +44 -0
  4. package/dist/docs/cli-deploy.md +58 -0
  5. package/dist/docs/cli-dev.md +63 -0
  6. package/dist/docs/cli-docs.md +47 -0
  7. package/dist/docs/cli-download.md +42 -0
  8. package/dist/docs/cli-egress.md +37 -0
  9. package/dist/docs/cli-feedback.md +51 -0
  10. package/dist/docs/cli-index.md +61 -0
  11. package/dist/docs/cli-init.md +46 -0
  12. package/dist/docs/cli-login.md +43 -0
  13. package/dist/docs/cli-logout.md +32 -0
  14. package/dist/docs/cli-logs.md +57 -0
  15. package/dist/docs/cli-open.md +41 -0
  16. package/dist/docs/cli-projects.md +43 -0
  17. package/dist/docs/cli-promote.md +44 -0
  18. package/dist/docs/cli-rollback.md +45 -0
  19. package/dist/docs/cli-skill.md +39 -0
  20. package/dist/docs/cli-staging.md +54 -0
  21. package/dist/docs/cli-token.md +47 -0
  22. package/dist/docs/cli-upgrade.md +43 -0
  23. package/dist/docs/cli-usage.md +43 -0
  24. package/dist/docs/cli-whoami.md +46 -0
  25. package/dist/docs/developers-agents-and-ai.md +95 -0
  26. package/dist/docs/developers-architecture.md +106 -0
  27. package/dist/docs/index.json +331 -0
  28. package/dist/docs/infrastructure.md +5 -8
  29. package/dist/docs/overview-faq.md +105 -0
  30. package/dist/docs/overview-how-it-works.md +81 -0
  31. package/dist/docs/overview-use-cases.md +76 -0
  32. package/dist/docs/overview-what-is-canopy.md +75 -0
  33. package/dist/docs/platform-ai-usage.md +65 -0
  34. package/dist/docs/platform-client-access.md +53 -0
  35. package/dist/docs/platform-deploys-and-rollbacks.md +93 -0
  36. package/dist/docs/platform-egress.md +75 -0
  37. package/dist/docs/platform-feedback.md +79 -0
  38. package/dist/docs/platform-members-and-access.md +71 -0
  39. package/dist/docs/platform-project-data.md +66 -0
  40. package/dist/docs/platform-schedules.md +90 -0
  41. package/dist/docs/platform-sites-and-profiles.md +61 -0
  42. package/dist/docs/static-serving.md +5 -8
  43. package/dist/docs/troubleshooting.md +6 -8
  44. package/dist/{docs-DQmUReJR.mjs → docs-DOiL2Sw-.mjs} +42 -20
  45. package/dist/feedback-BZG8E8zG.mjs +258 -0
  46. package/dist/index.mjs +10 -9
  47. package/dist/{logs-DBLDS3aQ.mjs → logs-DuzM-_Rn.mjs} +1 -1
  48. package/dist/{package-yVoO2pq4.mjs → package-BNiOibhj.mjs} +1 -1
  49. package/dist/{projects-D2fDeWNU.mjs → projects-C9TBoiW7.mjs} +5 -8
  50. package/dist/skill/canopy/SKILL.md +17 -8
  51. package/dist/{skill-12Ga-2UT.mjs → skill-DRTg-mJR.mjs} +1 -1
  52. package/dist/templates/worker/AGENTS.md +0 -3
  53. package/dist/templates/worker/README.md +1 -5
  54. package/dist/{upgrade-Du6U0xjp.mjs → upgrade-BDaM4M6Y.mjs} +1 -1
  55. 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. On Cloudflare
11
- there is no origin server running the project's code — the platform serves
12
- pre-built static files. (On the GCP target, `/api/*` requests dispatch to a
13
- per-project Cloud Run backend; everything else is still static serving. On
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; a per-project origin isolation (Phase 2 dev workers) would be the
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: on the Cloudflare target,
51
- per-project worker backends, dashboard schedules, and secret injection are not
52
- available and answer this error (the dashboard hides those panels). It's not a
53
- failure of your project — the feature doesn't exist on the target your platform
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. On the Cloudflare
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-yVoO2pq4.mjs";
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 — user/agent-facing guides, served live by the platform
13
- * (GET /control/v1/docs) so they never go stale, with a local cache and a bundled
14
- * snapshot as offline fallbacks.
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
- /** Topics from cache, else from the bundled snapshot's frontmatter. */
102
- function topicsFromFallback() {
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 cached.topics;
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
- return readdirSync(dir).filter((f) => /\.mdx?$/.test(f)).map((f) => {
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 listTopics(refresh) {
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.topics;
152
+ return data;
142
153
  }
143
154
  }
144
- return topicsFromFallback();
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 guides (offline-capable)"
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 topics = await listTopics(refresh);
181
- if (topics.length === 0) throw new Error("No docs available — check your connection, or reinstall the CLI.");
182
- const width = Math.max(...topics.map((t) => t.slug.length));
183
- console.log("Canopy docs — run `canopy docs <topic>`:\n");
184
- for (const t of topics) console.log(` ${t.slug.padEnd(width)} ${t.summary}`);
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 known = (await listTopics(false)).map((t) => t.slug).join(", ");
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-yVoO2pq4.mjs";
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-D2fDeWNU.mjs").then((m) => m.createCommand),
67
- init: () => import("./projects-D2fDeWNU.mjs").then((m) => m.initCommand),
68
- projects: () => import("./projects-D2fDeWNU.mjs").then((m) => m.projectsCommand),
69
- delete: () => import("./projects-D2fDeWNU.mjs").then((m) => m.deleteCommand),
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-DQmUReJR.mjs").then((m) => m.docsCommand),
82
- skill: () => import("./skill-12Ga-2UT.mjs").then((m) => m.skillCommand),
83
- upgrade: () => import("./upgrade-Du6U0xjp.mjs").then((m) => m.upgradeCommand),
84
- logs: () => import("./logs-DBLDS3aQ.mjs").then((m) => m.logsCommand)
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 — GCP sites only." }
37
+ fixes: { not_supported_on_target: "Worker logs aren't available on this site yet." }
38
38
  });
39
39
  }
40
40
  const logsCommand = defineCommand({
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.7.5";
2
+ var version = "0.7.7";
3
3
  //#endregion
4
4
  export { version as t };