@mediamonks/canopy-cli 0.7.37 → 0.7.39

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 (44) hide show
  1. package/README.md +5 -2
  2. package/dist/{ai-CjvrcBIz.mjs → ai-EyrXJC5z.mjs} +2 -2
  3. package/dist/{api-BPEYm9ts.mjs → api-ArfD6-t5.mjs} +40 -5
  4. package/dist/{browse-CBelCgnW.mjs → browse-DEhnJAPT.mjs} +1 -1
  5. package/dist/{db-CbqgjfYz.mjs → db-DGrCgj-R.mjs} +3 -3
  6. package/dist/{deploy-CqCyry4o.mjs → deploy-B7KItUnh.mjs} +3 -3
  7. package/dist/{dev-BtnEuAnS.mjs → dev-BMWj4NDZ.mjs} +5 -5
  8. package/dist/dev-BdSq6CGD.mjs +2 -0
  9. package/dist/docs/cli-create.md +2 -0
  10. package/dist/docs/cli-feedback.md +27 -1
  11. package/dist/docs/cli-projects.md +2 -0
  12. package/dist/docs/cli-secrets.md +2 -1
  13. package/dist/docs/cli-template.md +47 -0
  14. package/dist/docs/index.json +10 -0
  15. package/dist/docs/platform-schedules.md +21 -0
  16. package/dist/docs/templates.md +84 -0
  17. package/dist/{docs-DvCUaI1O.mjs → docs-Bplp4sOY.mjs} +7 -3
  18. package/dist/{egress-ButFsQ2N.mjs → egress-CzxRD6Tp.mjs} +2 -2
  19. package/dist/{feedback-CIjjiEaj.mjs → feedback-DFK0TXlt.mjs} +358 -14
  20. package/dist/index.mjs +31 -30
  21. package/dist/{local-BfqTheMP.mjs → local-BnfAEwso.mjs} +2 -2
  22. package/dist/{login-CtDWXICZ.mjs → login-B2FJGIrD.mjs} +24 -7
  23. package/dist/{logs-BltUi4Gp.mjs → logs-aJwWWaMx.mjs} +2 -2
  24. package/dist/{mcp-DTUJV4FT.mjs → mcp-DuT6_wei.mjs} +2 -2
  25. package/dist/{migrations-CF1iNZ7F.mjs → migrations-CG_O5Ghw.mjs} +1 -1
  26. package/dist/{open-DXo60Ikf.mjs → open-COFMburt.mjs} +1 -1
  27. package/dist/{package-BY69Tjq_.mjs → package-CJJpjVh_.mjs} +1 -1
  28. package/dist/project-template-DlzMx8Oj.mjs +2 -0
  29. package/dist/project-template-DtTfXyLI.mjs +125 -0
  30. package/dist/{projects-DPGRI-SI.mjs → projects-B3Wi01lw.mjs} +74 -8
  31. package/dist/{repo-CKIQrrHK.mjs → repo-4x-CJwCp.mjs} +22 -16
  32. package/dist/repo-CmbakFVi.mjs +2 -0
  33. package/dist/{schedule-DdMq5eXC.mjs → schedule-CbaDIJXr.mjs} +2 -2
  34. package/dist/{secrets-BQYaPSOT.mjs → secrets-CmY4dUHz.mjs} +6 -5
  35. package/dist/skill/canopy/SKILL.md +7 -4
  36. package/dist/{skill-B96cKy8K.mjs → skill-C2lGGz9n.mjs} +2 -2
  37. package/dist/{staging-CU4QrzFX.mjs → staging-ypR79MlD.mjs} +2 -2
  38. package/dist/{token-BWnLsWLg.mjs → token-CDrHQ5Xb.mjs} +2 -2
  39. package/dist/{ui-CHfKV6lG.mjs → ui-CaHgvvkJ.mjs} +1 -1
  40. package/dist/{upgrade-SfxmPh3F.mjs → upgrade-Caj1RsA7.mjs} +2 -2
  41. package/dist/{usage-Cqqvnzrf.mjs → usage-CP8rBVZi.mjs} +2 -2
  42. package/package.json +1 -1
  43. package/dist/dev-D_qzeLng.mjs +0 -2
  44. package/dist/repo-DvrIsubH.mjs +0 -2
@@ -1,21 +1,272 @@
1
- import { t as version } from "./package-BY69Tjq_.mjs";
1
+ import { t as version } from "./package-CJJpjVh_.mjs";
2
2
  import { a as projectDirArgs, c as resolveProject, d as tryResolveProject, n as siteConfig, u as tryReadCanopyJson } from "./site-DM2J10CB.mjs";
3
- import { a as apiUpload, i as apiRequest, r as apiDownload } from "./api-BPEYm9ts.mjs";
4
- import { r as reportErrors, t as formatDateTime } from "./ui-CHfKV6lG.mjs";
3
+ import { a as apiRequest, c as credentials, i as apiDownload, o as apiUpload, s as commonHeaders } from "./api-ArfD6-t5.mjs";
4
+ import { r as reportErrors, t as formatDateTime } from "./ui-CaHgvvkJ.mjs";
5
5
  import { defineCommand } from "citty";
6
6
  import { basename, extname, join } from "node:path";
7
7
  import { readFile, writeFile } from "node:fs/promises";
8
+ //#region src/sse.ts
9
+ /** Push-style parser: feed decoded text, collect events from the callback. */
10
+ function createSseParser(onEvent) {
11
+ let buffer = "";
12
+ let first = true;
13
+ let lastId;
14
+ let event = "";
15
+ let data = [];
16
+ let sawId = false;
17
+ const dispatch = () => {
18
+ if (data.length > 0 || sawId) onEvent({
19
+ id: lastId,
20
+ event: event || "message",
21
+ data: data.join("\n")
22
+ });
23
+ event = "";
24
+ data = [];
25
+ sawId = false;
26
+ };
27
+ const line = (text) => {
28
+ if (text === "") return dispatch();
29
+ if (text.startsWith(":")) return;
30
+ const colon = text.indexOf(":");
31
+ const field = colon === -1 ? text : text.slice(0, colon);
32
+ let value = colon === -1 ? "" : text.slice(colon + 1);
33
+ if (value.startsWith(" ")) value = value.slice(1);
34
+ if (field === "data") data.push(value);
35
+ else if (field === "event") event = value;
36
+ else if (field === "id" && !value.includes("\0")) {
37
+ lastId = value;
38
+ sawId = true;
39
+ }
40
+ };
41
+ return {
42
+ feed(text) {
43
+ buffer += text;
44
+ if (first && buffer.length > 0) {
45
+ if (buffer.startsWith("")) buffer = buffer.slice(1);
46
+ first = false;
47
+ }
48
+ let start = 0;
49
+ for (;;) {
50
+ const cr = buffer.indexOf("\r", start);
51
+ const lf = buffer.indexOf("\n", start);
52
+ if (cr === -1 && lf === -1) break;
53
+ const end = cr === -1 ? lf : lf === -1 ? cr : Math.min(cr, lf);
54
+ if (buffer[end] === "\r" && end === buffer.length - 1) break;
55
+ line(buffer.slice(start, end));
56
+ start = end + (buffer[end] === "\r" && buffer[end + 1] === "\n" ? 2 : 1);
57
+ }
58
+ buffer = buffer.slice(start);
59
+ },
60
+ /** The stream ended: a trailing lone "\r" still ends its line. */
61
+ end() {
62
+ if (buffer.endsWith("\r")) {
63
+ line(buffer.slice(0, -1));
64
+ buffer = "";
65
+ }
66
+ buffer = "";
67
+ event = "";
68
+ data = [];
69
+ sawId = false;
70
+ }
71
+ };
72
+ }
73
+ /** Read a fetch body as SSE events, one at a time. */
74
+ async function* readSse(body) {
75
+ const queue = [];
76
+ const parser = createSseParser((event) => queue.push(event));
77
+ const decoder = new TextDecoder();
78
+ const reader = body.getReader();
79
+ try {
80
+ for (;;) {
81
+ const { done, value } = await reader.read();
82
+ if (done) {
83
+ parser.feed(decoder.decode());
84
+ parser.end();
85
+ yield* queue.splice(0);
86
+ return;
87
+ }
88
+ parser.feed(decoder.decode(value, { stream: true }));
89
+ yield* queue.splice(0);
90
+ }
91
+ } finally {
92
+ reader.cancel().catch(() => {});
93
+ }
94
+ }
95
+ //#endregion
96
+ //#region src/feedback-watch.ts
97
+ /**
98
+ * `canopy feedback watch` (report-agent): follow report activity on one or
99
+ * more projects through GET /control/v1/feedback/events (SSE). The server
100
+ * ends each stream after a few minutes; this loop reconnects from the last
101
+ * cursor it saw, forever, until Ctrl-C or a refusal (401/403/404) it can't
102
+ * fix by retrying.
103
+ */
104
+ /**
105
+ * How a report or trail entry names its author: the person, or — when their
106
+ * agent did it — "Arjan van Wijk's agent (claude-code)".
107
+ */
108
+ function authorLabel(name, via, agent) {
109
+ const person = name || "unknown";
110
+ return via === "agent" && agent ? `${person}'s agent (${agent})` : person;
111
+ }
112
+ /** One text line per event: time, key, type, actor, then status change / internal / kind. */
113
+ function formatEvent(event) {
114
+ const type = event.type.replace(/^report\./, "");
115
+ const actor = authorLabel(event.actor?.name || event.actor?.email, event.via, event.agent);
116
+ const extra = [];
117
+ if (event.type === "report.created") {
118
+ if (event.kind) extra.push(event.kind);
119
+ if (event.status) extra.push(event.status);
120
+ }
121
+ if (event.status_to) extra.push(`${event.status_from ?? "?"} → ${event.status_to}`);
122
+ if (event.internal) extra.push("internal");
123
+ return [
124
+ formatDateTime(event.at),
125
+ event.key ?? event.item_id,
126
+ type,
127
+ actor,
128
+ ...extra.length > 0 ? [extra.join(" · ")] : []
129
+ ].join(" ");
130
+ }
131
+ /** The stream refused us for a reason retrying won't fix (401/403/404/other 4xx). */
132
+ var WatchRefused = class extends Error {
133
+ status;
134
+ code;
135
+ detail;
136
+ constructor(status, code, detail) {
137
+ super(detail ? `${status} ${code}: ${detail}` : `${status} ${code}`);
138
+ this.status = status;
139
+ this.code = code;
140
+ this.detail = detail;
141
+ this.name = "WatchRefused";
142
+ }
143
+ };
144
+ const BACKOFF_START_MS = 1e3;
145
+ const BACKOFF_MAX_MS = 3e4;
146
+ /** A stream that ends sooner than this without an event counts as a failure (no hot loop). */
147
+ const MIN_HEALTHY_STREAM_MS = 1e3;
148
+ function defaultSleep(ms, signal) {
149
+ return new Promise((resolve) => {
150
+ if (signal?.aborted) return resolve();
151
+ const timer = setTimeout(done, ms);
152
+ function done() {
153
+ clearTimeout(timer);
154
+ signal?.removeEventListener("abort", done);
155
+ resolve();
156
+ }
157
+ signal?.addEventListener("abort", done, { once: true });
158
+ });
159
+ }
160
+ /** The events URL for the next connection: the cursor, once we have one, replaces --since/--after. */
161
+ function eventsUrl(apiUrl, projects, cursor, since) {
162
+ const params = new URLSearchParams({ projects: projects.join(",") });
163
+ if (cursor) params.set("after", cursor);
164
+ else if (since) params.set("since", since);
165
+ return `${apiUrl}/control/v1/feedback/events?${params.toString()}`;
166
+ }
167
+ /**
168
+ * Follow the stream until `signal` aborts (resolves) or the server refuses
169
+ * the request (rejects with WatchRefused). Network errors, 5xx and 429 back
170
+ * off 1s, 2s, 4s… up to 30s; a stream that ends normally reconnects at once.
171
+ */
172
+ async function watchFeedback(options) {
173
+ const doFetch = options.fetch ?? globalThis.fetch;
174
+ const sleep = options.sleep ?? defaultSleep;
175
+ const now = options.now ?? Date.now;
176
+ const { signal } = options;
177
+ let cursor = options.after || void 0;
178
+ let backoff = BACKOFF_START_MS;
179
+ let announced = false;
180
+ const wait = async (reason) => {
181
+ options.err(`${reason} — reconnecting in ${Math.round(backoff / 1e3)}s…`);
182
+ await sleep(backoff, signal);
183
+ backoff = Math.min(backoff * 2, BACKOFF_MAX_MS);
184
+ };
185
+ while (!signal?.aborted) {
186
+ const startedAt = now();
187
+ let response;
188
+ try {
189
+ response = await doFetch(eventsUrl(options.apiUrl, options.projects, cursor, options.since), {
190
+ headers: {
191
+ ...options.headers?.(),
192
+ accept: "text/event-stream",
193
+ authorization: `Bearer ${options.token}`,
194
+ ...cursor ? { "last-event-id": cursor } : {}
195
+ },
196
+ signal
197
+ });
198
+ } catch (error) {
199
+ if (signal?.aborted) return;
200
+ await wait(`connection failed (${errorText(error)})`);
201
+ continue;
202
+ }
203
+ if (!response.ok) {
204
+ const payload = await response.json().catch(() => ({}));
205
+ const code = payload.error ?? `http_${response.status}`;
206
+ if (response.status >= 500 || response.status === 429) {
207
+ await wait(`stream answered ${response.status} ${code}`);
208
+ continue;
209
+ }
210
+ throw new WatchRefused(response.status, code, payload.detail);
211
+ }
212
+ if (!response.body) {
213
+ await wait("stream had no body");
214
+ continue;
215
+ }
216
+ if (!announced) {
217
+ announced = true;
218
+ options.err(`Watching ${options.projects.join(", ")}… (Ctrl-C to stop)`);
219
+ }
220
+ let delivered = 0;
221
+ let dropped;
222
+ try {
223
+ for await (const event of readSse(response.body)) {
224
+ if (event.id) cursor = event.id;
225
+ if (event.data === "") continue;
226
+ let data;
227
+ try {
228
+ data = JSON.parse(event.data);
229
+ } catch {
230
+ options.err(`skipping an event that is not JSON: ${event.data.slice(0, 120)}`);
231
+ continue;
232
+ }
233
+ delivered++;
234
+ options.out(options.json ? JSON.stringify({
235
+ ...data,
236
+ id: event.id ?? null
237
+ }) : formatEvent(data));
238
+ }
239
+ } catch (error) {
240
+ dropped = error ?? /* @__PURE__ */ new Error("stream error");
241
+ }
242
+ if (signal?.aborted) return;
243
+ const healthy = delivered > 0 || now() - startedAt >= MIN_HEALTHY_STREAM_MS;
244
+ if (healthy) backoff = BACKOFF_START_MS;
245
+ if (dropped !== void 0) await wait(`stream dropped (${errorText(dropped)})`);
246
+ else if (!healthy) await wait("stream closed immediately");
247
+ }
248
+ }
249
+ function errorText(error) {
250
+ if (error instanceof Error) {
251
+ const cause = error.cause;
252
+ return cause instanceof Error ? `${error.message}: ${cause.message}` : error.message;
253
+ }
254
+ return String(error);
255
+ }
256
+ //#endregion
8
257
  //#region src/feedback.ts
9
258
  /**
10
259
  * canopy feedback — the ticket-lite feedback system (docs/19, docs topic
11
260
  * `platform-feedback`). Two jobs:
12
261
  *
13
- * - Work a project's inbox: `list`, `show`, `comment`, `status` — investigate
262
+ * - Work a project's inbox: `list`, `show`, `comment`, `note`, `status` — investigate
14
263
  * what came in (`investigate` → comment the findings → `investigated`), and
15
264
  * build what a human marked `accepted`.
16
265
  * - File reports: `report` sends a bug/feedback item. With no `--project` it
17
266
  * lands on the **platform inbox** (canopy itself) — the right target when a
18
267
  * canopy command misbehaves while you're building something else.
268
+ * - Follow activity: `watch` streams new reports, comments and status
269
+ * changes as they happen (report-agent, feedback-watch.ts).
19
270
  */
20
271
  const STATUSES = [
21
272
  "investigate",
@@ -45,13 +296,16 @@ const IMAGE_TYPES = {
45
296
  * `--attach a.png,b.png` works as well, since agents write it both ways.
46
297
  */
47
298
  function attachedPaths(value) {
48
- const flag = "--attach";
299
+ return repeatedFlag("--attach", value);
300
+ }
301
+ /** Every occurrence of a repeatable string flag (citty keeps only the last), comma lists split. */
302
+ function repeatedFlag(flag, value) {
49
303
  const repeated = process.argv.flatMap((arg, index) => {
50
304
  if (arg === flag) {
51
305
  const next = process.argv[index + 1];
52
306
  return next && !next.startsWith("-") ? [next] : [];
53
307
  }
54
- return arg.startsWith(`${flag}=`) ? [arg.slice(9)] : [];
308
+ return arg.startsWith(`${flag}=`) ? [arg.slice(flag.length + 1)] : [];
55
309
  });
56
310
  return (repeated.length > 0 ? repeated : value === void 0 ? [] : [String(value)]).flatMap((entry) => entry.split(",")).map((path) => path.trim()).filter((path) => path !== "");
57
311
  }
@@ -180,7 +434,7 @@ const showCommand = defineCommand({
180
434
  const { item, comments } = detail;
181
435
  if (item.title) console.log(item.title);
182
436
  console.log(`${refOf(item)} · ${item.kind} · ${item.status} · via ${item.source} · ${formatDateTime(item.created_at)}`);
183
- console.log(`reported by ${item.reporter_name || item.reporter_email || "unknown"}` + (item.deployment_id ? ` on ${item.deployment_id}` : "") + (item.channel === "staging" ? " (staging)" : ""));
437
+ console.log(`reported by ${authorLabel(item.reporter_name || item.reporter_email, item.source === "agent" ? "agent" : null, item.agent)}` + (item.deployment_id ? ` on ${item.deployment_id}` : "") + (item.channel === "staging" ? " (staging)" : ""));
184
438
  console.log(`\n${item.body}\n`);
185
439
  if (item.context) {
186
440
  console.log("context:");
@@ -202,7 +456,7 @@ const showCommand = defineCommand({
202
456
  console.log("");
203
457
  }
204
458
  for (const entry of comments) {
205
- const author = entry.author_name || entry.author_email || "unknown";
459
+ const author = authorLabel(entry.author_name || entry.author_email, entry.via, entry.agent);
206
460
  const move = entry.status_to ? ` [${entry.status_from} → ${entry.status_to}]` : "";
207
461
  const internal = entry.internal ? " [internal]" : "";
208
462
  console.log(`--- ${author} · ${formatDateTime(entry.created_at)}${move}${internal}`);
@@ -243,11 +497,99 @@ const commentCommand = defineCommand({
243
497
  ...projectDirArgs
244
498
  },
245
499
  run: reportErrors("feedback comment", async ({ args }) => {
246
- await apiRequest("POST", `/projects/${await resolveItemSlug(args, args.id)}/feedback/${encodeURIComponent(args.id)}/comments`, {
247
- body: args.text,
248
- ...args.internal ? { internal: true } : {}
249
- });
250
- console.log(args.internal ? "Internal note added (hidden from the reporter)." : "Comment added.");
500
+ await postComment(args, Boolean(args.internal));
501
+ })
502
+ });
503
+ /** `comment` and `note` share one path: same ref resolution, same request. */
504
+ async function postComment(args, internal) {
505
+ await apiRequest("POST", `/projects/${await resolveItemSlug(args, args.id)}/feedback/${encodeURIComponent(args.id)}/comments`, {
506
+ body: args.text,
507
+ ...internal ? { internal: true } : {}
508
+ });
509
+ console.log(internal ? "Internal note added (hidden from the reporter)." : "Comment added.");
510
+ }
511
+ const noteCommand = defineCommand({
512
+ meta: {
513
+ name: "note",
514
+ description: "Add an internal note to an item — triager-only, never shown to the reporter (same as `comment --internal`; members only)"
515
+ },
516
+ args: {
517
+ id: {
518
+ type: "positional",
519
+ description: "Report key (my-project-17), or its number inside the project",
520
+ required: true
521
+ },
522
+ text: {
523
+ type: "positional",
524
+ description: "Note text",
525
+ required: true
526
+ },
527
+ ...projectDirArgs
528
+ },
529
+ run: reportErrors("feedback note", async ({ args }) => {
530
+ await postComment(args, true);
531
+ })
532
+ });
533
+ const watchCommand = defineCommand({
534
+ meta: {
535
+ name: "watch",
536
+ description: "Follow report activity live (new reports, comments, status changes) on one or more projects; reconnects on its own until Ctrl-C (members only)"
537
+ },
538
+ args: {
539
+ project: {
540
+ type: "string",
541
+ description: "Project slug to follow; repeat (or comma-separate) for several (default: canopy.json)"
542
+ },
543
+ since: {
544
+ type: "string",
545
+ description: "Start with activity since this ISO time (at most 7 days back)"
546
+ },
547
+ after: {
548
+ type: "string",
549
+ description: "Resume after this cursor (the `id` of the last --json line you handled)"
550
+ },
551
+ json: {
552
+ type: "boolean",
553
+ description: "One JSON object per line: the event plus its `id` cursor (diagnostics on stderr)"
554
+ }
555
+ },
556
+ run: reportErrors("feedback watch", async ({ args }) => {
557
+ let projects = [...new Set(repeatedFlag("--project", args.project))];
558
+ if (projects.length === 0) {
559
+ const { slug } = tryResolveProject({});
560
+ if (!slug) throw new Error("No project to watch — pass --project <slug> (repeat it for several), or run inside a project with a canopy.json.");
561
+ projects = [slug];
562
+ } else tryResolveProject({});
563
+ if (args.since && args.after) throw new Error("Pass --since or --after, not both (--after already says where to resume).");
564
+ if (args.since && Number.isNaN(Date.parse(args.since))) throw new Error(`--since must be an ISO time such as 2026-10-23T09:00:00Z (got "${args.since}").`);
565
+ const { apiUrl, token } = credentials();
566
+ const controller = new AbortController();
567
+ const stop = () => controller.abort();
568
+ process.once("SIGINT", stop);
569
+ process.once("SIGTERM", stop);
570
+ try {
571
+ await watchFeedback({
572
+ apiUrl,
573
+ token,
574
+ projects,
575
+ since: args.since,
576
+ after: args.after,
577
+ json: Boolean(args.json),
578
+ out: (line) => process.stdout.write(`${line}\n`),
579
+ err: (line) => process.stderr.write(`${line}\n`),
580
+ headers: commonHeaders,
581
+ signal: controller.signal
582
+ });
583
+ } catch (error) {
584
+ if (error instanceof WatchRefused) {
585
+ const fix = error.status === 401 ? " — run `canopy login` again." : error.status === 403 ? " — watch needs your own CLI login and access to every project you name." : "";
586
+ throw new Error(`the activity stream refused the request: ${error.message}${fix}`);
587
+ }
588
+ throw error;
589
+ } finally {
590
+ process.off("SIGINT", stop);
591
+ process.off("SIGTERM", stop);
592
+ }
251
593
  })
252
594
  });
253
595
  const statusCommand = defineCommand({
@@ -383,6 +725,7 @@ const feedbackCommand = defineCommand({
383
725
  list: listCommand,
384
726
  show: showCommand,
385
727
  comment: commentCommand,
728
+ note: noteCommand,
386
729
  attach: defineCommand({
387
730
  meta: {
388
731
  name: "attach",
@@ -439,7 +782,8 @@ const feedbackCommand = defineCommand({
439
782
  }),
440
783
  status: statusCommand,
441
784
  mine: mineCommand,
442
- report: reportCommand
785
+ report: reportCommand,
786
+ watch: watchCommand
443
787
  }
444
788
  });
445
789
  //#endregion
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as version } from "./package-BY69Tjq_.mjs";
2
+ import { t as version } from "./package-CJJpjVh_.mjs";
3
3
  import { r as siteHeaderLine, t as setInvocationProfile } from "./site-DM2J10CB.mjs";
4
4
  import { a as notifyOfUpdate, o as runRefresh } from "./version-check-CcsA1QjI.mjs";
5
5
  import { defineCommand, runMain } from "citty";
@@ -63,35 +63,36 @@ const main = defineCommand({
63
63
  description: "canopy — create and deploy vibecoded projects"
64
64
  },
65
65
  subCommands: {
66
- login: () => import("./login-CtDWXICZ.mjs").then((m) => m.loginCommand),
67
- whoami: () => import("./login-CtDWXICZ.mjs").then((m) => m.whoamiCommand),
68
- logout: () => import("./login-CtDWXICZ.mjs").then((m) => m.logoutCommand),
69
- create: () => import("./projects-DPGRI-SI.mjs").then((m) => m.createCommand),
70
- init: () => import("./projects-DPGRI-SI.mjs").then((m) => m.initCommand),
71
- projects: () => import("./projects-DPGRI-SI.mjs").then((m) => m.projectsCommand),
72
- delete: () => import("./projects-DPGRI-SI.mjs").then((m) => m.deleteCommand),
73
- dev: () => import("./dev-D_qzeLng.mjs").then((m) => m.devCommand),
74
- deploy: () => import("./deploy-CqCyry4o.mjs").then((m) => m.deployCommand),
75
- promote: () => import("./deploy-CqCyry4o.mjs").then((m) => m.promoteCommand),
76
- staging: () => import("./staging-CU4QrzFX.mjs").then((m) => m.stagingCommand),
77
- rollback: () => import("./deploy-CqCyry4o.mjs").then((m) => m.rollbackCommand),
78
- download: () => import("./deploy-CqCyry4o.mjs").then((m) => m.downloadCommand),
79
- token: () => import("./token-BWnLsWLg.mjs").then((m) => m.tokenCommand),
80
- egress: () => import("./egress-ButFsQ2N.mjs").then((m) => m.egressCommand),
81
- secrets: () => import("./secrets-BQYaPSOT.mjs").then((m) => m.secretsCommand),
82
- schedule: () => import("./schedule-DdMq5eXC.mjs").then((m) => m.scheduleCommand),
83
- feedback: () => import("./feedback-CIjjiEaj.mjs").then((m) => m.feedbackCommand),
84
- db: () => import("./db-CbqgjfYz.mjs").then((m) => m.dbCommand),
85
- usage: () => import("./usage-Cqqvnzrf.mjs").then((m) => m.usageCommand),
86
- ai: () => import("./ai-CjvrcBIz.mjs").then((m) => m.aiCommand),
87
- open: () => import("./open-DXo60Ikf.mjs").then((m) => m.openCommand),
88
- browse: () => import("./browse-CBelCgnW.mjs").then((m) => m.browseCommand),
89
- docs: () => import("./docs-DvCUaI1O.mjs").then((m) => m.docsCommand),
90
- skill: () => import("./skill-B96cKy8K.mjs").then((m) => m.skillCommand),
91
- upgrade: () => import("./upgrade-SfxmPh3F.mjs").then((m) => m.upgradeCommand),
92
- logs: () => import("./logs-BltUi4Gp.mjs").then((m) => m.logsCommand),
93
- mcp: () => import("./mcp-DTUJV4FT.mjs").then((m) => m.mcpCommand),
94
- repo: () => import("./repo-DvrIsubH.mjs").then((m) => m.repoCommand)
66
+ login: () => import("./login-B2FJGIrD.mjs").then((m) => m.loginCommand),
67
+ whoami: () => import("./login-B2FJGIrD.mjs").then((m) => m.whoamiCommand),
68
+ logout: () => import("./login-B2FJGIrD.mjs").then((m) => m.logoutCommand),
69
+ create: () => import("./projects-B3Wi01lw.mjs").then((m) => m.createCommand),
70
+ init: () => import("./projects-B3Wi01lw.mjs").then((m) => m.initCommand),
71
+ projects: () => import("./projects-B3Wi01lw.mjs").then((m) => m.projectsCommand),
72
+ delete: () => import("./projects-B3Wi01lw.mjs").then((m) => m.deleteCommand),
73
+ dev: () => import("./dev-BdSq6CGD.mjs").then((m) => m.devCommand),
74
+ deploy: () => import("./deploy-B7KItUnh.mjs").then((m) => m.deployCommand),
75
+ promote: () => import("./deploy-B7KItUnh.mjs").then((m) => m.promoteCommand),
76
+ staging: () => import("./staging-ypR79MlD.mjs").then((m) => m.stagingCommand),
77
+ rollback: () => import("./deploy-B7KItUnh.mjs").then((m) => m.rollbackCommand),
78
+ download: () => import("./deploy-B7KItUnh.mjs").then((m) => m.downloadCommand),
79
+ token: () => import("./token-CDrHQ5Xb.mjs").then((m) => m.tokenCommand),
80
+ egress: () => import("./egress-CzxRD6Tp.mjs").then((m) => m.egressCommand),
81
+ secrets: () => import("./secrets-CmY4dUHz.mjs").then((m) => m.secretsCommand),
82
+ schedule: () => import("./schedule-CbaDIJXr.mjs").then((m) => m.scheduleCommand),
83
+ feedback: () => import("./feedback-DFK0TXlt.mjs").then((m) => m.feedbackCommand),
84
+ db: () => import("./db-DGrCgj-R.mjs").then((m) => m.dbCommand),
85
+ usage: () => import("./usage-CP8rBVZi.mjs").then((m) => m.usageCommand),
86
+ ai: () => import("./ai-EyrXJC5z.mjs").then((m) => m.aiCommand),
87
+ open: () => import("./open-COFMburt.mjs").then((m) => m.openCommand),
88
+ browse: () => import("./browse-DEhnJAPT.mjs").then((m) => m.browseCommand),
89
+ docs: () => import("./docs-Bplp4sOY.mjs").then((m) => m.docsCommand),
90
+ skill: () => import("./skill-C2lGGz9n.mjs").then((m) => m.skillCommand),
91
+ upgrade: () => import("./upgrade-Caj1RsA7.mjs").then((m) => m.upgradeCommand),
92
+ logs: () => import("./logs-aJwWWaMx.mjs").then((m) => m.logsCommand),
93
+ mcp: () => import("./mcp-DuT6_wei.mjs").then((m) => m.mcpCommand),
94
+ repo: () => import("./repo-CmbakFVi.mjs").then((m) => m.repoCommand),
95
+ template: () => import("./project-template-DlzMx8Oj.mjs").then((m) => m.templateCommand)
95
96
  }
96
97
  });
97
98
  const quiet = process.argv[2] === "mcp" && process.argv[3] === "serve" || process.argv[2] === "repo" && process.argv[3] === "credential";
@@ -1,6 +1,6 @@
1
- import { n as apiDataRequest } from "./api-BPEYm9ts.mjs";
1
+ import { r as apiDataRequest } from "./api-ArfD6-t5.mjs";
2
2
  import { a as realtimeRouter, c as META_COLUMNS, l as blobRouter, n as WORKER_ATTEST_HEADER, o as dbRouter, s as hasPrivateNames } from "./src-CxzGOEhP.mjs";
3
- import { a as rewriteApiPath } from "./dev-BtnEuAnS.mjs";
3
+ import { a as rewriteApiPath } from "./dev-BMWj4NDZ.mjs";
4
4
  import { createRequire } from "node:module";
5
5
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
6
6
  import { dirname, join, relative, sep } from "node:path";
@@ -1,5 +1,6 @@
1
1
  import { _ as writeConfigFile, f as clearAllProfileTokens, g as upsertProfileAuth, h as setActiveProfile, m as readConfigFile, n as siteConfig, p as listBakedProfiles, v as getBakedProfile, y as isProfileId } from "./site-DM2J10CB.mjs";
2
- import { n as openBrowser, r as reportErrors } from "./ui-CHfKV6lG.mjs";
2
+ import { n as agentHeaders } from "./api-ArfD6-t5.mjs";
3
+ import { n as openBrowser, r as reportErrors } from "./ui-CaHgvvkJ.mjs";
3
4
  import { n as clearRepoTokenCache } from "./repo-credential-CNDRM-tG.mjs";
4
5
  import { defineCommand } from "citty";
5
6
  import { hostname } from "node:os";
@@ -37,16 +38,29 @@ const whoamiCommand = defineCommand({
37
38
  name: "whoami",
38
39
  description: "Show the current Canopy login"
39
40
  },
40
- run: reportErrors("whoami", async () => {
41
+ args: { json: {
42
+ type: "boolean",
43
+ description: "Machine-readable JSON: { profile, email, apiUrl, cloud, tokenExpiresAt, expired }"
44
+ } },
45
+ run: reportErrors("whoami", async ({ args }) => {
41
46
  const config = siteConfig();
42
47
  if (!config.token) throw new Error("Not logged in — run `canopy login`.");
48
+ const expired = config.tokenExpiresAt !== void 0 && Date.parse(config.tokenExpiresAt) < Date.now();
49
+ if (args.json) {
50
+ console.log(JSON.stringify({
51
+ profile: config.profileId ?? null,
52
+ email: config.email ?? null,
53
+ apiUrl: config.apiUrl,
54
+ cloud: config.cloud ?? null,
55
+ tokenExpiresAt: config.tokenExpiresAt ?? null,
56
+ expired
57
+ }));
58
+ return;
59
+ }
43
60
  console.log(config.email ?? "<unknown email>");
44
61
  if (config.cloud) console.log(`Cloud: ${config.cloud}`);
45
62
  console.log(`API: ${config.apiUrl}`);
46
- if (config.tokenExpiresAt !== void 0) {
47
- const expired = Date.parse(config.tokenExpiresAt) < Date.now();
48
- console.log(expired ? `Token EXPIRED ${config.tokenExpiresAt} — run \`canopy login\` again.` : `Token expires ${config.tokenExpiresAt}.`);
49
- }
63
+ if (config.tokenExpiresAt !== void 0) console.log(expired ? `Token EXPIRED ${config.tokenExpiresAt} — run \`canopy login\` again.` : `Token expires ${config.tokenExpiresAt}.`);
50
64
  })
51
65
  });
52
66
  /**
@@ -60,7 +74,10 @@ async function revokeRemote(apiUrl, token) {
60
74
  try {
61
75
  const res = await fetch(`${apiUrl}/control/v1/account/token`, {
62
76
  method: "DELETE",
63
- headers: { authorization: `Bearer ${token}` }
77
+ headers: {
78
+ authorization: `Bearer ${token}`,
79
+ ...agentHeaders()
80
+ }
64
81
  });
65
82
  if (res.ok || res.status === 401) return { ok: true };
66
83
  return {
@@ -1,6 +1,6 @@
1
1
  import { c as resolveProject, o as projectFlagArgs } from "./site-DM2J10CB.mjs";
2
- import { i as apiRequest, o as withContext } from "./api-BPEYm9ts.mjs";
3
- import { r as reportErrors } from "./ui-CHfKV6lG.mjs";
2
+ import { a as apiRequest, l as withContext } from "./api-ArfD6-t5.mjs";
3
+ import { r as reportErrors } from "./ui-CaHgvvkJ.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  //#region src/logs.ts
6
6
  /**
@@ -1,6 +1,6 @@
1
- import { t as version } from "./package-BY69Tjq_.mjs";
1
+ import { t as version } from "./package-CJJpjVh_.mjs";
2
2
  import { d as tryResolveProject, l as stagingUrl, n as siteConfig, s as projectUrl } from "./site-DM2J10CB.mjs";
3
- import { i as apiRequest, t as ApiError } from "./api-BPEYm9ts.mjs";
3
+ import { a as apiRequest, t as ApiError } from "./api-ArfD6-t5.mjs";
4
4
  import { t as liveDevServer } from "./dev-server-file-Dk9pk4AR.mjs";
5
5
  import { defineCommand } from "citty";
6
6
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
@@ -1,5 +1,5 @@
1
1
  import { u as tryReadCanopyJson } from "./site-DM2J10CB.mjs";
2
- import { n as apiDataRequest } from "./api-BPEYm9ts.mjs";
2
+ import { r as apiDataRequest } from "./api-ArfD6-t5.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-DM2J10CB.mjs";
2
- import { n as openBrowser, r as reportErrors } from "./ui-CHfKV6lG.mjs";
2
+ import { n as openBrowser, r as reportErrors } from "./ui-CaHgvvkJ.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  //#region src/open.ts
5
5
  /**
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.7.37";
2
+ var version = "0.7.39";
3
3
  //#endregion
4
4
  export { version as t };
@@ -0,0 +1,2 @@
1
+ import { a as templateCommand } from "./project-template-DtTfXyLI.mjs";
2
+ export { templateCommand };