@aiaiai-pt/manda-cli 0.0.0-stage → 0.1.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.
@@ -0,0 +1,710 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { api, exitCodeForStatus } from "./client.js";
3
+ import { isJsonMode, printJson, printError, printTable, printKeyValue, readStdin, } from "./output.js";
4
+ function die(status, message, current) {
5
+ const exit = exitCodeForStatus(status);
6
+ printError(status, message, exit, current);
7
+ process.exit(exit);
8
+ }
9
+ async function handleError(res, refetchCurrent) {
10
+ if (!res.ok) {
11
+ let current;
12
+ if (res.status === 409 && refetchCurrent) {
13
+ try {
14
+ const fresh = await refetchCurrent();
15
+ current = fresh;
16
+ }
17
+ catch {
18
+ /* best effort */
19
+ }
20
+ }
21
+ die(res.status, res.message, current);
22
+ }
23
+ // TypeScript: this should never be reached
24
+ throw new Error("unreachable");
25
+ }
26
+ // stdin can feed exactly one flag per invocation: a second "-" would wait on a stream
27
+ // that is already closed and never settle.
28
+ let stdinConsumed = false;
29
+ async function resolveText(value) {
30
+ if (value !== "-")
31
+ return value;
32
+ if (stdinConsumed) {
33
+ printError(0, "stdin (-) can feed only one flag per call", 1);
34
+ process.exit(1);
35
+ }
36
+ stdinConsumed = true;
37
+ return (await readStdin()).trim();
38
+ }
39
+ // Fetch current revision for a resource, unless --revision is given
40
+ async function getRevision(config, path, flags) {
41
+ const explicit = flags["revision"];
42
+ if (explicit !== undefined)
43
+ return Number(explicit);
44
+ const res = await api(config, path);
45
+ if (!res.ok)
46
+ die(res.status, res.message);
47
+ const data = res.data;
48
+ // submissions have .submission.revision, topics have .topic.revision
49
+ if (typeof data === "object" && data !== null) {
50
+ if ("submission" in data)
51
+ return data.submission.revision;
52
+ if ("topic" in data)
53
+ return data.topic.revision;
54
+ if ("revision" in data)
55
+ return data.revision;
56
+ }
57
+ return 0;
58
+ }
59
+ function refetchSubmission(config, id) {
60
+ return async () => {
61
+ const r = await api(config, `/api/agent/v1/submissions/${id}`);
62
+ return r.ok ? r.data : undefined;
63
+ };
64
+ }
65
+ function refetchTopic(config, id) {
66
+ return async () => {
67
+ const r = await api(config, `/api/agent/v1/topics/${id}`);
68
+ return r.ok ? r.data : undefined;
69
+ };
70
+ }
71
+ // --- whoami ---
72
+ export async function whoami(config, flags) {
73
+ const res = await api(config, "/api/agent/v1/me");
74
+ if (!res.ok)
75
+ return handleError(res);
76
+ if (isJsonMode(flags)) {
77
+ printJson(res.data);
78
+ }
79
+ else {
80
+ const d = res.data;
81
+ const agent = d.agent;
82
+ printKeyValue([
83
+ ["email", d.email],
84
+ ["domain", d.domain],
85
+ ["agent", agent.name],
86
+ ]);
87
+ }
88
+ }
89
+ // --- projects ---
90
+ export async function projects(config, flags) {
91
+ const res = await api(config, "/api/agent/v1/projects");
92
+ if (!res.ok)
93
+ return handleError(res);
94
+ if (isJsonMode(flags)) {
95
+ printJson(res.data);
96
+ }
97
+ else {
98
+ printTable(res.data.projects, [
99
+ { key: "id", label: "ID", width: 38 },
100
+ { key: "name", label: "NAME" },
101
+ ]);
102
+ }
103
+ }
104
+ // --- inbox ---
105
+ export async function inbox(config, flags) {
106
+ const query = {};
107
+ if (flags["project"])
108
+ query["project_id"] = String(flags["project"]);
109
+ if (flags["state"])
110
+ query["review_state"] = String(flags["state"]);
111
+ if (flags["page"])
112
+ query["page"] = String(flags["page"]);
113
+ if (flags["limit"])
114
+ query["limit"] = String(flags["limit"]);
115
+ if (flags["q"])
116
+ query["q"] = String(flags["q"]);
117
+ let assignee = flags["assignee"] ? String(flags["assignee"]) : undefined;
118
+ if (assignee === "me") {
119
+ const meRes = await api(config, "/api/agent/v1/me");
120
+ if (!meRes.ok)
121
+ return handleError(meRes);
122
+ assignee = meRes.data.email;
123
+ }
124
+ if (assignee)
125
+ query["assignee_email"] = assignee;
126
+ const res = await api(config, "/api/agent/v1/submissions", { query });
127
+ if (!res.ok)
128
+ return handleError(res);
129
+ if (isJsonMode(flags)) {
130
+ printJson(res.data);
131
+ }
132
+ else {
133
+ printTable(res.data.submissions, [
134
+ { key: "id", label: "ID", width: 38 },
135
+ { key: "type", label: "TYPE", width: 8 },
136
+ { key: "status", label: "STATUS", width: 14 },
137
+ { key: "message", label: "MESSAGE" },
138
+ ]);
139
+ }
140
+ }
141
+ // --- next ---
142
+ export async function next(config, flags) {
143
+ const query = {};
144
+ if (flags["project"])
145
+ query["project_id"] = String(flags["project"]);
146
+ const res = await api(config, "/api/agent/v1/next", { query });
147
+ if (!res.ok)
148
+ return handleError(res);
149
+ if (isJsonMode(flags)) {
150
+ printJson(res.data);
151
+ }
152
+ else {
153
+ printTable(res.data.items.map((item) => ({
154
+ ...item,
155
+ owner: item.assignee_email ?? "unassigned",
156
+ topic: item.topics.map((t) => t.title).join(", ") || "-",
157
+ })), [
158
+ { key: "id", label: "ID", width: 38 },
159
+ { key: "review_state", label: "STATE", width: 12 },
160
+ { key: "owner", label: "OWNER", width: 24 },
161
+ { key: "topic", label: "TOPIC", width: 32 },
162
+ { key: "message", label: "MESSAGE" },
163
+ ]);
164
+ }
165
+ }
166
+ // --- instructions ---
167
+ /** A block for CLAUDE.md or AGENTS.md: the loop a harness runs with manda as its coordination layer. */
168
+ export const INSTRUCTIONS = `## Manda: customer feedback and work coordination
169
+
170
+ Manda is the ledger of what customers reported and what they have been told. Use the \`manda\`
171
+ CLI (\`npm i -g @aiaiai-pt/manda-cli\`, \`MANDA_TOKEN\` set) as the coordination layer while
172
+ working in this codebase, alone or with a person. Every call prints JSON when piped; exit codes:
173
+ 0 ok, 1 usage, 2 auth, 3 conflict (reload and decide again), 4 not found, 5 other.
174
+
175
+ The loop:
176
+
177
+ 1. \`manda next --json\` lists open reports you own or nobody owns, in priority order. Take the first.
178
+ 2. \`manda report show <id> --json\` reads the report and its timeline. \`manda report assign <id> --to me\` takes it
179
+ (owned reports rank first in \`next\`; an unowned one you are working on will sink below fresh reports), then
180
+ \`manda report ack <id>\` starts review.
181
+ 3. Group it: \`manda topic list --json\` to find the topic it belongs to, then
182
+ \`manda topic link-report <topic> --report <id>\`; or \`manda topic create --project <project> --title "<customer intent>"\`
183
+ and \`manda topic transition <topic> plan\` then \`start\` when you begin.
184
+ 4. Do the work in the repository. Put \`Manda-Report: <id>\` and \`Manda-Topic: <topic>\` as commit trailers
185
+ and \`manda topic link-work <topic> --tracker github --issue <pr number> --issue-url <pr url>\` once a PR exists.
186
+ 5. Tell the customer only when it is true: \`manda report comment <id> --audience customer --outcome resolved --body "<what changed, in their words>"\`.
187
+ Internal notes use \`--audience internal\`. Never resolve without a customer-facing explanation.
188
+ 6. \`manda topic transition <topic> complete\` when the change is available to customers; \`manda topic announce <topic> --body "<progress>"\` for public progress.
189
+
190
+ Rules: read before you write (every write checks the revision); do not reprioritise by editing reports, reorder topics
191
+ (\`manda topic reorder\`) with the person who owns the roadmap; a 3 exit means someone else changed it, re-read and decide again.
192
+ `;
193
+ export function instructions() {
194
+ process.stdout.write(INSTRUCTIONS);
195
+ }
196
+ // --- report ---
197
+ export async function reportShow(config, id, flags) {
198
+ const res = await api(config, `/api/agent/v1/submissions/${id}`);
199
+ if (!res.ok)
200
+ return handleError(res);
201
+ if (isJsonMode(flags)) {
202
+ printJson(res.data);
203
+ }
204
+ else {
205
+ const d = res.data;
206
+ const s = d.submission;
207
+ printKeyValue([
208
+ ["id", s.id],
209
+ ["type", s.type],
210
+ ["status", s.status],
211
+ ["message", s.message],
212
+ ["revision", s.revision],
213
+ ]);
214
+ const updates = d.updates;
215
+ if (updates && updates.length > 0) {
216
+ process.stdout.write("\nUpdates:\n");
217
+ printTable(updates, [
218
+ { key: "kind", label: "KIND", width: 14 },
219
+ { key: "author_email", label: "AUTHOR", width: 30 },
220
+ { key: "body", label: "BODY" },
221
+ ]);
222
+ }
223
+ }
224
+ }
225
+ export async function reportCapture(config, flags) {
226
+ const projectId = flags["project"];
227
+ const type = flags["type"];
228
+ const message = await resolveText(flags["message"]);
229
+ if (!projectId || !type || !message) {
230
+ printError(0, "Missing required flag: --project, --type, --message", 1);
231
+ process.exit(1);
232
+ }
233
+ const body = {
234
+ project_id: String(projectId),
235
+ type: String(type),
236
+ message,
237
+ };
238
+ if (flags["name"])
239
+ body.user_name = String(flags["name"]);
240
+ if (flags["email"])
241
+ body.user_email = String(flags["email"]);
242
+ if (flags["source-label"])
243
+ body.source_label = String(flags["source-label"]);
244
+ const res = await api(config, "/api/agent/v1/submissions", {
245
+ method: "POST",
246
+ body,
247
+ });
248
+ if (!res.ok)
249
+ return handleError(res);
250
+ if (isJsonMode(flags)) {
251
+ printJson(res.data);
252
+ }
253
+ else {
254
+ const d = res.data;
255
+ process.stdout.write(`Created report ${d.id}\n`);
256
+ }
257
+ }
258
+ export async function reportAssign(config, id, flags) {
259
+ let to = flags["to"];
260
+ if (to === undefined) {
261
+ printError(0, "Missing required flag: --to", 1);
262
+ process.exit(1);
263
+ }
264
+ if (to === "me") {
265
+ const meRes = await api(config, "/api/agent/v1/me");
266
+ if (!meRes.ok)
267
+ return handleError(meRes);
268
+ to = meRes.data.email;
269
+ }
270
+ else if (to === "none") {
271
+ to = undefined; // null means unassign
272
+ }
273
+ const revision = await getRevision(config, `/api/agent/v1/submissions/${id}`, flags);
274
+ const body = {
275
+ revision,
276
+ action: "assign",
277
+ assignee_email: to === undefined ? null : String(to),
278
+ };
279
+ const res = await api(config, `/api/agent/v1/submissions/${id}`, {
280
+ method: "PATCH",
281
+ body,
282
+ });
283
+ if (!res.ok)
284
+ return handleError(res, refetchSubmission(config, id));
285
+ if (isJsonMode(flags))
286
+ printJson(res.data);
287
+ else
288
+ process.stdout.write(`Assigned ${id}\n`);
289
+ }
290
+ export async function reportAck(config, id, flags) {
291
+ const revision = await getRevision(config, `/api/agent/v1/submissions/${id}`, flags);
292
+ const res = await api(config, `/api/agent/v1/submissions/${id}`, {
293
+ method: "PATCH",
294
+ body: { revision, action: "acknowledge" },
295
+ });
296
+ if (!res.ok)
297
+ return handleError(res, refetchSubmission(config, id));
298
+ if (isJsonMode(flags))
299
+ printJson(res.data);
300
+ else
301
+ process.stdout.write(`Acknowledged ${id}\n`);
302
+ }
303
+ export async function reportResolve(config, id, flags) {
304
+ const resolution = await resolveText(flags["resolution"]);
305
+ if (!resolution) {
306
+ printError(0, "Missing required flag: --resolution", 1);
307
+ process.exit(1);
308
+ }
309
+ const revision = await getRevision(config, `/api/agent/v1/submissions/${id}`, flags);
310
+ const res = await api(config, `/api/agent/v1/submissions/${id}`, {
311
+ method: "PATCH",
312
+ body: { revision, action: "resolve", resolution },
313
+ });
314
+ if (!res.ok)
315
+ return handleError(res, refetchSubmission(config, id));
316
+ if (isJsonMode(flags))
317
+ printJson(res.data);
318
+ else
319
+ process.stdout.write(`Resolved ${id}\n`);
320
+ }
321
+ export async function reportReopen(config, id, flags) {
322
+ const revision = await getRevision(config, `/api/agent/v1/submissions/${id}`, flags);
323
+ const res = await api(config, `/api/agent/v1/submissions/${id}`, {
324
+ method: "PATCH",
325
+ body: { revision, action: "reopen" },
326
+ });
327
+ if (!res.ok)
328
+ return handleError(res, refetchSubmission(config, id));
329
+ if (isJsonMode(flags))
330
+ printJson(res.data);
331
+ else
332
+ process.stdout.write(`Reopened ${id}\n`);
333
+ }
334
+ export async function reportLink(config, id, flags) {
335
+ const url = flags["work-url"];
336
+ const none = flags["none"];
337
+ if (!url && !none) {
338
+ printError(0, "Missing required flag: --work-url or --none", 1);
339
+ process.exit(1);
340
+ }
341
+ const revision = await getRevision(config, `/api/agent/v1/submissions/${id}`, flags);
342
+ const res = await api(config, `/api/agent/v1/submissions/${id}`, {
343
+ method: "PATCH",
344
+ body: {
345
+ revision,
346
+ action: "link",
347
+ manual_work_url: none ? null : String(url),
348
+ },
349
+ });
350
+ if (!res.ok)
351
+ return handleError(res, refetchSubmission(config, id));
352
+ if (isJsonMode(flags))
353
+ printJson(res.data);
354
+ else
355
+ process.stdout.write(`Linked ${id}\n`);
356
+ }
357
+ export async function reportComment(config, id, flags) {
358
+ const body = await resolveText(flags["body"]);
359
+ if (!body) {
360
+ printError(0, "Missing required flag: --body", 1);
361
+ process.exit(1);
362
+ }
363
+ const requestId = flags["request-id"]
364
+ ? String(flags["request-id"])
365
+ : randomUUID();
366
+ const revision = await getRevision(config, `/api/agent/v1/submissions/${id}`, flags);
367
+ const payload = {
368
+ revision,
369
+ audience: flags["audience"] || "internal",
370
+ body,
371
+ request_id: requestId,
372
+ };
373
+ if (flags["outcome"])
374
+ payload.outcome = String(flags["outcome"]);
375
+ const res = await api(config, `/api/agent/v1/submissions/${id}/updates`, {
376
+ method: "POST",
377
+ body: payload,
378
+ });
379
+ if (!res.ok)
380
+ return handleError(res, refetchSubmission(config, id));
381
+ if (isJsonMode(flags))
382
+ printJson(res.data);
383
+ else
384
+ process.stdout.write(`Comment added to ${id}\n`);
385
+ }
386
+ // --- topic ---
387
+ export async function topicList(config, flags) {
388
+ const query = {};
389
+ if (flags["project"])
390
+ query["project_id"] = String(flags["project"]);
391
+ const res = await api(config, "/api/agent/v1/topics", { query });
392
+ if (!res.ok)
393
+ return handleError(res);
394
+ if (isJsonMode(flags)) {
395
+ printJson(res.data);
396
+ }
397
+ else {
398
+ printTable(res.data.topics, [
399
+ { key: "id", label: "ID", width: 38 },
400
+ { key: "state", label: "STATE", width: 14 },
401
+ { key: "title", label: "TITLE" },
402
+ ]);
403
+ }
404
+ }
405
+ export async function topicShow(config, id, flags) {
406
+ const res = await api(config, `/api/agent/v1/topics/${id}`);
407
+ if (!res.ok)
408
+ return handleError(res);
409
+ if (isJsonMode(flags)) {
410
+ printJson(res.data);
411
+ }
412
+ else {
413
+ const d = res.data;
414
+ const t = d.topic;
415
+ printKeyValue([
416
+ ["id", t.id],
417
+ ["title", t.title],
418
+ ["state", t.state],
419
+ ["revision", t.revision],
420
+ ]);
421
+ }
422
+ }
423
+ export async function topicCreate(config, flags) {
424
+ const projectId = flags["project"];
425
+ const title = await resolveText(flags["title"]);
426
+ if (!projectId || !title) {
427
+ printError(0, "Missing required flag: --project, --title", 1);
428
+ process.exit(1);
429
+ }
430
+ const body = {
431
+ project_id: String(projectId),
432
+ title,
433
+ };
434
+ const description = await resolveText(flags["description"]);
435
+ if (description)
436
+ body.description = description;
437
+ const res = await api(config, "/api/agent/v1/topics", {
438
+ method: "POST",
439
+ body,
440
+ });
441
+ if (!res.ok)
442
+ return handleError(res);
443
+ if (isJsonMode(flags)) {
444
+ printJson(res.data);
445
+ }
446
+ else {
447
+ const d = res.data;
448
+ process.stdout.write(`Created topic ${d.id}\n`);
449
+ }
450
+ }
451
+ export async function topicTransition(config, id, event, flags) {
452
+ const revision = await getRevision(config, `/api/agent/v1/topics/${id}`, flags);
453
+ const body = {
454
+ revision,
455
+ action: "transition",
456
+ event,
457
+ };
458
+ const text = await resolveText(flags["body"]);
459
+ if (text)
460
+ body.body = text;
461
+ const res = await api(config, `/api/agent/v1/topics/${id}`, {
462
+ method: "PATCH",
463
+ body,
464
+ });
465
+ if (!res.ok)
466
+ return handleError(res, refetchTopic(config, id));
467
+ if (isJsonMode(flags))
468
+ printJson(res.data);
469
+ else
470
+ process.stdout.write(`Transitioned ${id} via ${event}\n`);
471
+ }
472
+ export async function topicNote(config, id, flags) {
473
+ const body = await resolveText(flags["body"]);
474
+ if (!body) {
475
+ printError(0, "Missing required flag: --body", 1);
476
+ process.exit(1);
477
+ }
478
+ const revision = await getRevision(config, `/api/agent/v1/topics/${id}`, flags);
479
+ const res = await api(config, `/api/agent/v1/topics/${id}`, {
480
+ method: "PATCH",
481
+ body: { revision, action: "note", body },
482
+ });
483
+ if (!res.ok)
484
+ return handleError(res, refetchTopic(config, id));
485
+ if (isJsonMode(flags))
486
+ printJson(res.data);
487
+ else
488
+ process.stdout.write(`Note added to ${id}\n`);
489
+ }
490
+ export async function topicEdit(config, id, flags) {
491
+ const title = await resolveText(flags["title"]);
492
+ const description = await resolveText(flags["description"]);
493
+ if (!title || description === undefined) {
494
+ printError(0, "Missing required flag: --title, --description", 1);
495
+ process.exit(1);
496
+ }
497
+ const revision = await getRevision(config, `/api/agent/v1/topics/${id}`, flags);
498
+ const res = await api(config, `/api/agent/v1/topics/${id}`, {
499
+ method: "PATCH",
500
+ body: { revision, action: "edit", title, description: description || "" },
501
+ });
502
+ if (!res.ok)
503
+ return handleError(res, refetchTopic(config, id));
504
+ if (isJsonMode(flags))
505
+ printJson(res.data);
506
+ else
507
+ process.stdout.write(`Edited ${id}\n`);
508
+ }
509
+ export async function topicLinkReport(config, id, flags) {
510
+ const reportId = flags["report"];
511
+ if (!reportId) {
512
+ printError(0, "Missing required flag: --report", 1);
513
+ process.exit(1);
514
+ }
515
+ const revision = await getRevision(config, `/api/agent/v1/topics/${id}`, flags);
516
+ const res = await api(config, `/api/agent/v1/topics/${id}`, {
517
+ method: "PATCH",
518
+ body: { revision, action: "link_report", submission_id: String(reportId) },
519
+ });
520
+ if (!res.ok)
521
+ return handleError(res, refetchTopic(config, id));
522
+ if (isJsonMode(flags))
523
+ printJson(res.data);
524
+ else
525
+ process.stdout.write(`Linked report ${reportId} to ${id}\n`);
526
+ }
527
+ export async function topicUnlinkReport(config, id, flags) {
528
+ const reportId = flags["report"];
529
+ if (!reportId) {
530
+ printError(0, "Missing required flag: --report", 1);
531
+ process.exit(1);
532
+ }
533
+ const revision = await getRevision(config, `/api/agent/v1/topics/${id}`, flags);
534
+ const res = await api(config, `/api/agent/v1/topics/${id}`, {
535
+ method: "PATCH",
536
+ body: {
537
+ revision,
538
+ action: "unlink_report",
539
+ submission_id: String(reportId),
540
+ },
541
+ });
542
+ if (!res.ok)
543
+ return handleError(res, refetchTopic(config, id));
544
+ if (isJsonMode(flags))
545
+ printJson(res.data);
546
+ else
547
+ process.stdout.write(`Unlinked report ${reportId} from ${id}\n`);
548
+ }
549
+ export async function topicLinkWork(config, id, flags) {
550
+ const tracker = flags["tracker"];
551
+ const issue = flags["issue"];
552
+ const url = flags["issue-url"];
553
+ if (!tracker || !issue || !url) {
554
+ printError(0, "Missing required flag: --tracker, --issue, --issue-url", 1);
555
+ process.exit(1);
556
+ }
557
+ const revision = await getRevision(config, `/api/agent/v1/topics/${id}`, flags);
558
+ const res = await api(config, `/api/agent/v1/topics/${id}`, {
559
+ method: "PATCH",
560
+ body: {
561
+ revision,
562
+ action: "link_work",
563
+ tracker: String(tracker),
564
+ issue_id: String(issue),
565
+ issue_url: String(url),
566
+ },
567
+ });
568
+ if (!res.ok)
569
+ return handleError(res, refetchTopic(config, id));
570
+ if (isJsonMode(flags))
571
+ printJson(res.data);
572
+ else
573
+ process.stdout.write(`Linked work to ${id}\n`);
574
+ }
575
+ export async function topicUnlinkWork(config, id, flags) {
576
+ const tracker = flags["tracker"];
577
+ const issue = flags["issue"];
578
+ if (!tracker || !issue) {
579
+ printError(0, "Missing required flag: --tracker, --issue", 1);
580
+ process.exit(1);
581
+ }
582
+ const revision = await getRevision(config, `/api/agent/v1/topics/${id}`, flags);
583
+ const res = await api(config, `/api/agent/v1/topics/${id}`, {
584
+ method: "PATCH",
585
+ body: {
586
+ revision,
587
+ action: "unlink_work",
588
+ tracker: String(tracker),
589
+ issue_id: String(issue),
590
+ },
591
+ });
592
+ if (!res.ok)
593
+ return handleError(res, refetchTopic(config, id));
594
+ if (isJsonMode(flags))
595
+ printJson(res.data);
596
+ else
597
+ process.stdout.write(`Unlinked work from ${id}\n`);
598
+ }
599
+ export async function topicPublish(config, id, flags) {
600
+ const revision = await getRevision(config, `/api/agent/v1/topics/${id}`, flags);
601
+ const body = { revision, action: "publish" };
602
+ const publicTitle = await resolveText(flags["public-title"]);
603
+ const publicSummary = await resolveText(flags["public-summary"]);
604
+ if (publicTitle)
605
+ body.public_title = publicTitle;
606
+ if (publicSummary)
607
+ body.public_summary = publicSummary;
608
+ const res = await api(config, `/api/agent/v1/topics/${id}`, {
609
+ method: "PATCH",
610
+ body,
611
+ });
612
+ if (!res.ok)
613
+ return handleError(res, refetchTopic(config, id));
614
+ if (isJsonMode(flags))
615
+ printJson(res.data);
616
+ else
617
+ process.stdout.write(`Published ${id}\n`);
618
+ }
619
+ export async function topicUnpublish(config, id, flags) {
620
+ const revision = await getRevision(config, `/api/agent/v1/topics/${id}`, flags);
621
+ const res = await api(config, `/api/agent/v1/topics/${id}`, {
622
+ method: "PATCH",
623
+ body: { revision, action: "unpublish" },
624
+ });
625
+ if (!res.ok)
626
+ return handleError(res, refetchTopic(config, id));
627
+ if (isJsonMode(flags))
628
+ printJson(res.data);
629
+ else
630
+ process.stdout.write(`Unpublished ${id}\n`);
631
+ }
632
+ export async function topicEditPublic(config, id, flags) {
633
+ const publicTitle = await resolveText(flags["public-title"]);
634
+ const publicSummary = await resolveText(flags["public-summary"]);
635
+ if (!publicTitle || publicSummary === undefined) {
636
+ printError(0, "Missing required flag: --public-title, --public-summary", 1);
637
+ process.exit(1);
638
+ }
639
+ const revision = await getRevision(config, `/api/agent/v1/topics/${id}`, flags);
640
+ const res = await api(config, `/api/agent/v1/topics/${id}`, {
641
+ method: "PATCH",
642
+ body: {
643
+ revision,
644
+ action: "edit_public",
645
+ public_title: publicTitle,
646
+ public_summary: publicSummary || "",
647
+ },
648
+ });
649
+ if (!res.ok)
650
+ return handleError(res, refetchTopic(config, id));
651
+ if (isJsonMode(flags))
652
+ printJson(res.data);
653
+ else
654
+ process.stdout.write(`Edited public view of ${id}\n`);
655
+ }
656
+ export async function topicReorder(config, topicIds, flags) {
657
+ const projectId = flags["project"];
658
+ if (!projectId) {
659
+ printError(0, "Missing required flag: --project", 1);
660
+ process.exit(1);
661
+ }
662
+ if (topicIds.length === 0) {
663
+ printError(0, "At least one topic ID is required", 1);
664
+ process.exit(1);
665
+ }
666
+ const res = await api(config, "/api/agent/v1/topics", {
667
+ method: "PATCH",
668
+ body: { project_id: String(projectId), topic_ids: topicIds },
669
+ });
670
+ if (!res.ok)
671
+ return handleError(res);
672
+ if (isJsonMode(flags))
673
+ printJson(res.data);
674
+ else
675
+ process.stdout.write(`Reordered ${topicIds.length} topics\n`);
676
+ }
677
+ export async function topicAnnounce(config, id, flags) {
678
+ const body = await resolveText(flags["body"]);
679
+ if (!body) {
680
+ printError(0, "Missing required flag: --body", 1);
681
+ process.exit(1);
682
+ }
683
+ const res = await api(config, `/api/agent/v1/topics/${id}/announcements`, {
684
+ method: "POST",
685
+ body: { body },
686
+ });
687
+ if (!res.ok)
688
+ return handleError(res);
689
+ if (isJsonMode(flags))
690
+ printJson(res.data);
691
+ else
692
+ process.stdout.write(`Announcement added to ${id}\n`);
693
+ }
694
+ export async function topicUnannounce(config, id, flags) {
695
+ const announcementId = flags["announcement"];
696
+ if (!announcementId) {
697
+ printError(0, "Missing required flag: --announcement", 1);
698
+ process.exit(1);
699
+ }
700
+ const res = await api(config, `/api/agent/v1/topics/${id}/announcements`, {
701
+ method: "DELETE",
702
+ body: { announcement_id: announcementId },
703
+ });
704
+ if (!res.ok)
705
+ return handleError(res);
706
+ if (isJsonMode(flags))
707
+ printJson(res.data);
708
+ else
709
+ process.stdout.write(`Announcement ${announcementId} removed from ${id}\n`);
710
+ }