@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.
- package/README.md +5 -2
- package/dist/{ai-CjvrcBIz.mjs → ai-EyrXJC5z.mjs} +2 -2
- package/dist/{api-BPEYm9ts.mjs → api-ArfD6-t5.mjs} +40 -5
- package/dist/{browse-CBelCgnW.mjs → browse-DEhnJAPT.mjs} +1 -1
- package/dist/{db-CbqgjfYz.mjs → db-DGrCgj-R.mjs} +3 -3
- package/dist/{deploy-CqCyry4o.mjs → deploy-B7KItUnh.mjs} +3 -3
- package/dist/{dev-BtnEuAnS.mjs → dev-BMWj4NDZ.mjs} +5 -5
- package/dist/dev-BdSq6CGD.mjs +2 -0
- package/dist/docs/cli-create.md +2 -0
- package/dist/docs/cli-feedback.md +27 -1
- package/dist/docs/cli-projects.md +2 -0
- package/dist/docs/cli-secrets.md +2 -1
- package/dist/docs/cli-template.md +47 -0
- package/dist/docs/index.json +10 -0
- package/dist/docs/platform-schedules.md +21 -0
- package/dist/docs/templates.md +84 -0
- package/dist/{docs-DvCUaI1O.mjs → docs-Bplp4sOY.mjs} +7 -3
- package/dist/{egress-ButFsQ2N.mjs → egress-CzxRD6Tp.mjs} +2 -2
- package/dist/{feedback-CIjjiEaj.mjs → feedback-DFK0TXlt.mjs} +358 -14
- package/dist/index.mjs +31 -30
- package/dist/{local-BfqTheMP.mjs → local-BnfAEwso.mjs} +2 -2
- package/dist/{login-CtDWXICZ.mjs → login-B2FJGIrD.mjs} +24 -7
- package/dist/{logs-BltUi4Gp.mjs → logs-aJwWWaMx.mjs} +2 -2
- package/dist/{mcp-DTUJV4FT.mjs → mcp-DuT6_wei.mjs} +2 -2
- package/dist/{migrations-CF1iNZ7F.mjs → migrations-CG_O5Ghw.mjs} +1 -1
- package/dist/{open-DXo60Ikf.mjs → open-COFMburt.mjs} +1 -1
- package/dist/{package-BY69Tjq_.mjs → package-CJJpjVh_.mjs} +1 -1
- package/dist/project-template-DlzMx8Oj.mjs +2 -0
- package/dist/project-template-DtTfXyLI.mjs +125 -0
- package/dist/{projects-DPGRI-SI.mjs → projects-B3Wi01lw.mjs} +74 -8
- package/dist/{repo-CKIQrrHK.mjs → repo-4x-CJwCp.mjs} +22 -16
- package/dist/repo-CmbakFVi.mjs +2 -0
- package/dist/{schedule-DdMq5eXC.mjs → schedule-CbaDIJXr.mjs} +2 -2
- package/dist/{secrets-BQYaPSOT.mjs → secrets-CmY4dUHz.mjs} +6 -5
- package/dist/skill/canopy/SKILL.md +7 -4
- package/dist/{skill-B96cKy8K.mjs → skill-C2lGGz9n.mjs} +2 -2
- package/dist/{staging-CU4QrzFX.mjs → staging-ypR79MlD.mjs} +2 -2
- package/dist/{token-BWnLsWLg.mjs → token-CDrHQ5Xb.mjs} +2 -2
- package/dist/{ui-CHfKV6lG.mjs → ui-CaHgvvkJ.mjs} +1 -1
- package/dist/{upgrade-SfxmPh3F.mjs → upgrade-Caj1RsA7.mjs} +2 -2
- package/dist/{usage-Cqqvnzrf.mjs → usage-CP8rBVZi.mjs} +2 -2
- package/package.json +1 -1
- package/dist/dev-D_qzeLng.mjs +0 -2
- package/dist/repo-DvrIsubH.mjs +0 -2
|
@@ -1,21 +1,272 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
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
|
|
4
|
-
import { r as reportErrors, t as formatDateTime } from "./ui-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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-
|
|
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-
|
|
67
|
-
whoami: () => import("./login-
|
|
68
|
-
logout: () => import("./login-
|
|
69
|
-
create: () => import("./projects-
|
|
70
|
-
init: () => import("./projects-
|
|
71
|
-
projects: () => import("./projects-
|
|
72
|
-
delete: () => import("./projects-
|
|
73
|
-
dev: () => import("./dev-
|
|
74
|
-
deploy: () => import("./deploy-
|
|
75
|
-
promote: () => import("./deploy-
|
|
76
|
-
staging: () => import("./staging-
|
|
77
|
-
rollback: () => import("./deploy-
|
|
78
|
-
download: () => import("./deploy-
|
|
79
|
-
token: () => import("./token-
|
|
80
|
-
egress: () => import("./egress-
|
|
81
|
-
secrets: () => import("./secrets-
|
|
82
|
-
schedule: () => import("./schedule-
|
|
83
|
-
feedback: () => import("./feedback-
|
|
84
|
-
db: () => import("./db-
|
|
85
|
-
usage: () => import("./usage-
|
|
86
|
-
ai: () => import("./ai-
|
|
87
|
-
open: () => import("./open-
|
|
88
|
-
browse: () => import("./browse-
|
|
89
|
-
docs: () => import("./docs-
|
|
90
|
-
skill: () => import("./skill-
|
|
91
|
-
upgrade: () => import("./upgrade-
|
|
92
|
-
logs: () => import("./logs-
|
|
93
|
-
mcp: () => import("./mcp-
|
|
94
|
-
repo: () => import("./repo-
|
|
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 {
|
|
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-
|
|
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
|
|
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
|
-
|
|
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: {
|
|
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 {
|
|
3
|
-
import { r as reportErrors } from "./ui-
|
|
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-
|
|
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 {
|
|
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 {
|
|
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-
|
|
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
|
/**
|