@uic-coe-connect/cli 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/reports.js +92 -2
- package/package.json +1 -1
package/dist/commands/reports.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename } from "node:path";
|
|
1
3
|
import { CliError } from "../client.js";
|
|
2
4
|
import { details, emit, info, table } from "../output.js";
|
|
3
5
|
import { register } from "../registry.js";
|
|
@@ -14,6 +16,31 @@ import { register } from "../registry.js";
|
|
|
14
16
|
*/
|
|
15
17
|
const STATUSES = ["open", "planned", "in-progress", "done", "declined"];
|
|
16
18
|
const CLOSED = ["done", "declined"];
|
|
19
|
+
function humanSize(bytes) {
|
|
20
|
+
if (bytes < 1024)
|
|
21
|
+
return `${bytes} B`;
|
|
22
|
+
if (bytes < 1024 * 1024)
|
|
23
|
+
return `${Math.round(bytes / 1024)} KB`;
|
|
24
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
25
|
+
}
|
|
26
|
+
/** Every file on the thread, wherever it hangs — the whole list is what you search. */
|
|
27
|
+
function allAttachments(report) {
|
|
28
|
+
return [...(report.attachments ?? []), ...(report.comments ?? []).flatMap((c) => c.attachments ?? [])];
|
|
29
|
+
}
|
|
30
|
+
/** Match an attachment by id or by filename, the way `files download` matches. */
|
|
31
|
+
function findAttachment(report, ref) {
|
|
32
|
+
const all = allAttachments(report);
|
|
33
|
+
const byId = all.find((a) => a.id === ref);
|
|
34
|
+
if (byId)
|
|
35
|
+
return byId;
|
|
36
|
+
const matches = all.filter((a) => a.name.toLowerCase().includes(ref.toLowerCase()));
|
|
37
|
+
if (matches.length === 1)
|
|
38
|
+
return matches[0];
|
|
39
|
+
if (matches.length > 1) {
|
|
40
|
+
throw new CliError(`"${ref}" matches ${matches.length} attachments: ${matches.map((a) => a.name).join(", ")}`, 4, "Use the exact attachment id.");
|
|
41
|
+
}
|
|
42
|
+
throw new CliError(`No attachment "${ref}" on that report.`, 4, all.length === 0 ? "It has no attachments." : `It has: ${all.map((a) => a.name).join(", ")}`);
|
|
43
|
+
}
|
|
17
44
|
function assertStatus(value) {
|
|
18
45
|
if (!STATUSES.includes(value)) {
|
|
19
46
|
throw new CliError(`Unknown status "${value}".`, 6, `Use one of: ${STATUSES.join(", ")}`);
|
|
@@ -120,10 +147,21 @@ export function registerReportCommands() {
|
|
|
120
147
|
...(r.pageUrl ? [["Page", r.pageUrl]] : []),
|
|
121
148
|
]);
|
|
122
149
|
process.stdout.write(`\n${r.body}\n`);
|
|
150
|
+
for (const a of r.attachments ?? []) {
|
|
151
|
+
process.stdout.write(` [attachment] ${a.name} (${humanSize(a.size)}) ${a.id}\n`);
|
|
152
|
+
}
|
|
123
153
|
for (const comment of r.comments ?? []) {
|
|
124
154
|
const change = comment.statusTo ? ` [→ ${comment.statusTo}]` : "";
|
|
125
155
|
process.stdout.write(`\n--- ${comment.authorName ?? comment.authorNetid} · ${comment.createdAt}${change}\n` +
|
|
126
156
|
(comment.body ? `${comment.body}\n` : ""));
|
|
157
|
+
for (const a of comment.attachments ?? []) {
|
|
158
|
+
process.stdout.write(` [attachment] ${a.name} (${humanSize(a.size)}) ${a.id}\n`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const files = allAttachments(r);
|
|
162
|
+
if (files.length > 0) {
|
|
163
|
+
info("");
|
|
164
|
+
info(`${files.length} attachment(s) — fetch one with: coe reports download ${r.id} <name>`);
|
|
127
165
|
}
|
|
128
166
|
if (!data.canRespond) {
|
|
129
167
|
info("");
|
|
@@ -162,14 +200,51 @@ export function registerReportCommands() {
|
|
|
162
200
|
const { report } = await client().request("POST", `/reports/${id}/comments`, { body: args.flag("note") ?? "", status });
|
|
163
201
|
emit({ report }, () => info(`✓ ${report.id} is now ${report.status}`));
|
|
164
202
|
},
|
|
203
|
+
}, {
|
|
204
|
+
name: "reports attach",
|
|
205
|
+
summary: "Attach a file to a report",
|
|
206
|
+
usage: "reports attach <id> <path>",
|
|
207
|
+
details: [
|
|
208
|
+
"Screenshots, logs, spreadsheets — anything that shows the problem.",
|
|
209
|
+
"Up to 10 MB each, 20 per report. It lands on the report itself; use the",
|
|
210
|
+
"web UI to attach a file to one particular reply.",
|
|
211
|
+
],
|
|
212
|
+
async run({ args, client }) {
|
|
213
|
+
const id = args.arg(0, "id");
|
|
214
|
+
const filePath = args.arg(1, "path");
|
|
215
|
+
let bytes;
|
|
216
|
+
try {
|
|
217
|
+
bytes = readFileSync(filePath);
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
throw new CliError(`Can't read ${filePath}: ${error instanceof Error ? error.message : String(error)}`, 4);
|
|
221
|
+
}
|
|
222
|
+
const file = await client().upload(`/reports/${id}/attachments`, basename(filePath), bytes);
|
|
223
|
+
emit({ attachment: file }, () => info(`✓ Attached ${file.name} (${humanSize(file.size)}) to ${id}`));
|
|
224
|
+
},
|
|
225
|
+
}, {
|
|
226
|
+
name: "reports download",
|
|
227
|
+
summary: "Download an attachment from a report",
|
|
228
|
+
usage: "reports download <id> <name|attachment-id> [--out <path>]",
|
|
229
|
+
async run({ args, client }) {
|
|
230
|
+
const c = client();
|
|
231
|
+
const id = args.arg(0, "id");
|
|
232
|
+
const { report } = await c.request("GET", `/reports/${id}`);
|
|
233
|
+
const file = findAttachment(report, args.arg(1, "name"));
|
|
234
|
+
const bytes = await c.download(`/reports/${id}/attachments/${file.id}`);
|
|
235
|
+
const out = args.flag("out") ?? file.name;
|
|
236
|
+
writeFileSync(out, bytes);
|
|
237
|
+
emit({ reportId: id, attachment: file.name, out, bytes: bytes.length }, () => info(`✓ Wrote ${out} (${humanSize(bytes.length)})`));
|
|
238
|
+
},
|
|
165
239
|
}, {
|
|
166
240
|
name: "reports file",
|
|
167
241
|
summary: "File a bug report or feature request",
|
|
168
|
-
usage: "reports file <app> <title> [--details <text>] [--request]",
|
|
242
|
+
usage: "reports file <app> <title> [--details <text>] [--request] [--attach <path>]",
|
|
169
243
|
details: [
|
|
170
244
|
" <app> The app it's about, or `coeconnect` for the portal itself",
|
|
171
245
|
" --details The body of the report (defaults to the title)",
|
|
172
246
|
" --request File it as a feature request rather than a bug",
|
|
247
|
+
" --attach A file to attach — a screenshot, a log excerpt",
|
|
173
248
|
],
|
|
174
249
|
async run({ args, client }) {
|
|
175
250
|
const c = client();
|
|
@@ -182,7 +257,22 @@ export function registerReportCommands() {
|
|
|
182
257
|
title,
|
|
183
258
|
body: args.flag("details") ?? title,
|
|
184
259
|
});
|
|
185
|
-
|
|
260
|
+
// Attached after the report exists, because that is what it hangs off.
|
|
261
|
+
// A failure here must not read as "the report wasn't filed" — it was.
|
|
262
|
+
const attach = args.flag("attach");
|
|
263
|
+
let attachment = null;
|
|
264
|
+
if (attach) {
|
|
265
|
+
try {
|
|
266
|
+
attachment = await c.upload(`/reports/${report.id}/attachments`, basename(attach), readFileSync(attach));
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
info(`✓ Filed ${report.id} — ${report.title}`);
|
|
270
|
+
throw new CliError(`The report was filed, but ${attach} did not attach: ` +
|
|
271
|
+
(error instanceof Error ? error.message : String(error)), 1, `Try again with: coe reports attach ${report.id} ${attach}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
emit({ report, attachment }, () => info(`✓ Filed ${report.id} — ${report.title}` +
|
|
275
|
+
(attachment ? ` (attached ${attachment.name})` : "")));
|
|
186
276
|
},
|
|
187
277
|
});
|
|
188
278
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uic-coe-connect/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "The coe CLI — manage COEConnect apps (pipelines, deploys, access, env) from a terminal, with browser-approved auth. Designed to be driven by an AI agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|