@bli-cockpit/cli 0.2.241 → 0.2.243
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/adapters/git-state.js +1 -0
- package/dist/adapters/raw-evidence-collection-state.js +22 -11
- package/dist/adapters/raw-evidence-git-diff.js +1 -0
- package/dist/agent-rules.js +8 -0
- package/dist/autostart-contract.js +12 -0
- package/dist/autostart-darwin-plist.js +2 -2
- package/dist/autostart-darwin.js +1 -1
- package/dist/autostart-remind.js +245 -0
- package/dist/autostart-self-heal.js +17 -1
- package/dist/autostart-windows-scripts.js +7 -7
- package/dist/autostart.js +16 -15
- package/dist/commands/approvals-check-where.js +14 -0
- package/dist/commands/approvals-render.js +8 -1
- package/dist/commands/approvals.js +3 -2
- package/dist/commands/autostart-command.js +10 -0
- package/dist/commands/autostart-heal.js +36 -1
- package/dist/commands/brain.js +1 -1
- package/dist/commands/cal-agenda.js +44 -0
- package/dist/commands/cal-availability-share.js +97 -0
- package/dist/commands/cal-booking.js +147 -0
- package/dist/commands/cal-join.js +62 -0
- package/dist/commands/cal-links.js +68 -0
- package/dist/commands/cal-people.js +136 -0
- package/dist/commands/cal-remind.js +176 -0
- package/dist/commands/cal-reminders.js +33 -0
- package/dist/commands/cal-suggest.js +89 -0
- package/dist/commands/cal.js +41 -1
- package/dist/commands/cycle-args.js +127 -0
- package/dist/commands/cycles.js +117 -0
- package/dist/commands/docs-comments-args.js +85 -0
- package/dist/commands/docs-comments.js +128 -0
- package/dist/commands/docs-files-args.js +49 -0
- package/dist/commands/docs-files.js +179 -0
- package/dist/commands/docs-formatting.js +45 -0
- package/dist/commands/docs-inbox-args.js +93 -0
- package/dist/commands/docs-inbox.js +126 -0
- package/dist/commands/docs-notion.js +5 -0
- package/dist/commands/docs-read.js +2 -1
- package/dist/commands/docs-recovery.js +173 -0
- package/dist/commands/docs-refs-args.js +104 -0
- package/dist/commands/docs-refs.js +153 -0
- package/dist/commands/docs-search-args.js +79 -0
- package/dist/commands/docs-search.js +64 -0
- package/dist/commands/docs-sync-args.js +82 -0
- package/dist/commands/docs-sync.js +197 -0
- package/dist/commands/docs-tree.js +136 -0
- package/dist/commands/docs.js +85 -51
- package/dist/commands/doctor-update.js +4 -1
- package/dist/commands/inbox-args.js +81 -0
- package/dist/commands/inbox.js +88 -0
- package/dist/commands/issue-batch.js +61 -0
- package/dist/commands/issue-relations.js +151 -0
- package/dist/commands/issue-write.js +38 -5
- package/dist/commands/issue.js +32 -1
- package/dist/commands/local-args-tower-approvals.js +16 -4
- package/dist/commands/local-args-tower-cal-booking.js +81 -0
- package/dist/commands/local-args-tower-cal-links.js +42 -0
- package/dist/commands/local-args-tower-cal-remind.js +74 -0
- package/dist/commands/local-args-tower-cal-share.js +41 -0
- package/dist/commands/local-args-tower-cal.js +81 -8
- package/dist/commands/local-args-tower-docs-msg.js +98 -9
- package/dist/commands/local-args-tower-msg.js +279 -10
- package/dist/commands/local-args-tower-search.js +32 -5
- package/dist/commands/local-args-tower-work.js +110 -34
- package/dist/commands/local-args-tower.js +1 -0
- package/dist/commands/local-args.js +2 -0
- package/dist/commands/local-auth.js +1 -1
- package/dist/commands/local-help-commands-tower-ops.js +2 -0
- package/dist/commands/local-help-commands-tower.js +48 -32
- package/dist/commands/local-help-commands.js +1 -0
- package/dist/commands/local-help-cycles.js +9 -0
- package/dist/commands/local-help-docs-files.js +10 -0
- package/dist/commands/local-help-docs-formatting.js +7 -0
- package/dist/commands/local-help-docs-inbox.js +10 -0
- package/dist/commands/local-help-docs-recovery.js +10 -0
- package/dist/commands/local-help-docs-refs.js +11 -0
- package/dist/commands/local-help-docs-sync.js +9 -0
- package/dist/commands/local-help-inbox.js +9 -0
- package/dist/commands/local-help-project.js +21 -0
- package/dist/commands/local-help-templates.js +9 -0
- package/dist/commands/local-help-views.js +10 -0
- package/dist/commands/local-help-work-notify.js +6 -0
- package/dist/commands/local-help.js +4 -2
- package/dist/commands/local.js +2 -0
- package/dist/commands/mcp-stdio-probe.js +1 -0
- package/dist/commands/msg-activity.js +134 -0
- package/dist/commands/msg-channel-args.js +92 -0
- package/dist/commands/msg-channel.js +125 -0
- package/dist/commands/msg-context-args.js +67 -0
- package/dist/commands/msg-context.js +65 -0
- package/dist/commands/msg-dm.js +28 -0
- package/dist/commands/msg-draft.js +193 -0
- package/dist/commands/msg-file.js +185 -0
- package/dist/commands/msg-mark-args.js +78 -0
- package/dist/commands/msg-mark.js +148 -0
- package/dist/commands/msg-notify-args.js +100 -0
- package/dist/commands/msg-notify.js +122 -0
- package/dist/commands/msg-schedule-args.js +114 -0
- package/dist/commands/msg-schedule.js +226 -0
- package/dist/commands/msg-search-args.js +71 -0
- package/dist/commands/msg-search.js +48 -0
- package/dist/commands/msg-write.js +15 -1
- package/dist/commands/msg.js +77 -39
- package/dist/commands/project-args.js +82 -0
- package/dist/commands/project.js +71 -7
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/push.js +50 -0
- package/dist/commands/search.js +23 -0
- package/dist/commands/session-sync-failures.js +17 -4
- package/dist/commands/session-sync-health.js +7 -2
- package/dist/commands/session-sync-hold.js +42 -1
- package/dist/commands/session-sync-upload.js +2 -0
- package/dist/commands/session-sync.js +1 -0
- package/dist/commands/sync-doctor.js +14 -1
- package/dist/commands/sync-report.js +1 -1
- package/dist/commands/template-args.js +122 -0
- package/dist/commands/templates.js +162 -0
- package/dist/commands/view-args.js +131 -0
- package/dist/commands/views.js +138 -0
- package/dist/commands/work-notify-args.js +67 -0
- package/dist/commands/work-notify.js +63 -0
- package/dist/delivery-hold-notice.js +34 -0
- package/dist/log-rotation.js +2 -2
- package/dist/notifier.js +83 -0
- package/dist/process-runner.js +4 -0
- package/dist/repo-identity-git.js +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit docs comments` and `cockpit docs comment` argument parsing (BLI-21701, docs parity step 7).
|
|
3
|
+
*
|
|
4
|
+
* `comments <doc> [--status open|resolved|all] [--cursor <c>] [--json]` reads the discussion.
|
|
5
|
+
* `comment <doc> "<text>" | -` starts a thread; exactly one of `--reply <thread>`, `--edit <comment>`,
|
|
6
|
+
* `--delete <comment>`, `--resolve <thread>` or `--reopen <thread>` changes the verb. Text is the second
|
|
7
|
+
* positional, or `-` for stdin. The door checks authority; this file only checks shape.
|
|
8
|
+
*/
|
|
9
|
+
import { optionalNonEmpty, parseNamedArgs } from "./local-arg-values.js";
|
|
10
|
+
import { optionalUrl } from "./local-arg-values.js";
|
|
11
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
12
|
+
const ACTION_FLAGS = ["--reply", "--edit", "--delete", "--resolve", "--reopen"];
|
|
13
|
+
const VALUE_FLAGS = ["--home", "--dashboard-url", "--status", "--cursor", "--request-id", ...ACTION_FLAGS];
|
|
14
|
+
function idFlag(flag, raw) {
|
|
15
|
+
const value = optionalNonEmpty(raw);
|
|
16
|
+
if (value !== undefined && !UUID.test(value))
|
|
17
|
+
throw new Error(`${flag} takes an id (a UUID), from \`cockpit docs comments <doc> --json\`.`);
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
export function parseDocsCommentsArgs(args) {
|
|
21
|
+
const reading = args[0] === "comments";
|
|
22
|
+
const values = parseNamedArgs(args, {
|
|
23
|
+
allowedFlags: [...VALUE_FLAGS, "--json"],
|
|
24
|
+
valueFlags: VALUE_FLAGS,
|
|
25
|
+
});
|
|
26
|
+
const rest = values.positionals.slice(1);
|
|
27
|
+
const docRef = optionalNonEmpty(rest[0]);
|
|
28
|
+
const verb = reading ? "comments" : "comment";
|
|
29
|
+
if (!docRef)
|
|
30
|
+
throw new Error(`docs ${verb} needs a document id or slug.`);
|
|
31
|
+
const base = {
|
|
32
|
+
kind: "docs",
|
|
33
|
+
action: reading ? "comments" : "comment",
|
|
34
|
+
docRef,
|
|
35
|
+
clearParent: false,
|
|
36
|
+
bodyStdin: false,
|
|
37
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
38
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
39
|
+
json: values.booleans.has("--json"),
|
|
40
|
+
};
|
|
41
|
+
const chosen = ACTION_FLAGS.filter((flag) => values.flags.has(flag));
|
|
42
|
+
if (reading) {
|
|
43
|
+
if (chosen.length > 0 || values.flags.has("--request-id")) {
|
|
44
|
+
throw new Error("docs comments only reads. Use `cockpit docs comment` to write.");
|
|
45
|
+
}
|
|
46
|
+
if (rest.length > 1)
|
|
47
|
+
throw new Error(`docs comments takes one document reference, not ${rest.length}.`);
|
|
48
|
+
const status = optionalNonEmpty(values.flags.get("--status"));
|
|
49
|
+
if (status !== undefined && status !== "open" && status !== "resolved" && status !== "all") {
|
|
50
|
+
throw new Error(`--status takes open, resolved or all, not "${status}".`);
|
|
51
|
+
}
|
|
52
|
+
const cursor = optionalNonEmpty(values.flags.get("--cursor"));
|
|
53
|
+
return { ...base, ...(status ? { commentStatus: status } : {}), ...(cursor ? { cursor } : {}) };
|
|
54
|
+
}
|
|
55
|
+
if (values.flags.has("--status") || values.flags.has("--cursor"))
|
|
56
|
+
throw new Error("--status and --cursor belong to `cockpit docs comments`.");
|
|
57
|
+
if (chosen.length > 1)
|
|
58
|
+
throw new Error(`docs comment takes exactly one action, not ${chosen.join(" and ")}.`);
|
|
59
|
+
const requestId = optionalNonEmpty(values.flags.get("--request-id"));
|
|
60
|
+
if (requestId && !UUID.test(requestId))
|
|
61
|
+
throw new Error("--request-id must be a UUID.");
|
|
62
|
+
const reply = idFlag("--reply", values.flags.get("--reply"));
|
|
63
|
+
const edit = idFlag("--edit", values.flags.get("--edit"));
|
|
64
|
+
const remove = idFlag("--delete", values.flags.get("--delete"));
|
|
65
|
+
const resolve = idFlag("--resolve", values.flags.get("--resolve"));
|
|
66
|
+
const reopen = idFlag("--reopen", values.flags.get("--reopen"));
|
|
67
|
+
const needsText = remove === undefined && resolve === undefined && reopen === undefined;
|
|
68
|
+
const text = rest.length > 1 ? rest.slice(1).join(" ") : undefined;
|
|
69
|
+
if (needsText && !text)
|
|
70
|
+
throw new Error('docs comment needs the text: `cockpit docs comment <doc> "<text>"`, or `-` to read it from stdin.');
|
|
71
|
+
if (!needsText && text)
|
|
72
|
+
throw new Error(`${chosen[0]} takes no text.`);
|
|
73
|
+
if (requestId && !(needsText && edit === undefined))
|
|
74
|
+
throw new Error("--request-id belongs to starting a thread or replying.");
|
|
75
|
+
return {
|
|
76
|
+
...base,
|
|
77
|
+
...(text ? { commentText: text } : {}),
|
|
78
|
+
...(requestId ? { requestId } : {}),
|
|
79
|
+
...(reply ? { commentReply: reply } : {}),
|
|
80
|
+
...(edit ? { commentEdit: edit } : {}),
|
|
81
|
+
...(remove ? { commentDelete: remove } : {}),
|
|
82
|
+
...(resolve ? { commentResolve: resolve } : {}),
|
|
83
|
+
...(reopen ? { commentReopen: reopen } : {}),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit docs comments` and `cockpit docs comment` — page discussions from a terminal (BLI-21701).
|
|
3
|
+
* A sibling of `docs.ts`, like `docs-share.ts`.
|
|
4
|
+
*
|
|
5
|
+
* `comments <id|slug> [--status open|resolved|all] [--cursor <c>]` → `GET /api/docs/documents/[id]/comments`
|
|
6
|
+
* `comment <id|slug> "<text>" | -` → `POST /api/docs/documents/[id]/comments`
|
|
7
|
+
* `comment <id|slug> "<text>" --reply <thread>` → `POST .../comments` with thread_id
|
|
8
|
+
* `comment <id|slug> "<text>" --edit <comment>` → `PATCH .../comments/[commentId]`
|
|
9
|
+
* `comment <id|slug> --delete <comment>` → `DELETE .../comments/[commentId]`
|
|
10
|
+
* `comment <id|slug> --resolve|--reopen <thread>` → `PATCH .../threads/[threadId]`
|
|
11
|
+
*
|
|
12
|
+
* The door's `reason` labels (`not_commenter`, `not_comment_author`, `comment_not_found`, `thread_not_found`,
|
|
13
|
+
* `invalid_body`, ...) come back verbatim. Text is the second argument, or `-` for stdin.
|
|
14
|
+
*/
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
16
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
17
|
+
import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
|
|
18
|
+
import { resolveDocId } from "./docs.js";
|
|
19
|
+
const TAG = "[docs comments cli]";
|
|
20
|
+
const READ_DEADLINE_MS = 30_000;
|
|
21
|
+
const WRITE_DEADLINE_MS = 60_000;
|
|
22
|
+
const TEXT_MAX_CHARS = 10_000;
|
|
23
|
+
async function resolveOrFail(command, door) {
|
|
24
|
+
const ref = command.docRef ?? "";
|
|
25
|
+
const resolved = await resolveDocId(door, ref);
|
|
26
|
+
if (resolved.status === "list_failed")
|
|
27
|
+
return { exit: failAgentDoor(door, TAG, resolved.reason, resolved.detail) };
|
|
28
|
+
if (resolved.status === "not_found") {
|
|
29
|
+
return { exit: failAgentDoor(door, TAG, "document_not_found_or_unreadable", `That document does not exist, or you cannot read it: ${ref}`) };
|
|
30
|
+
}
|
|
31
|
+
return { id: resolved.id };
|
|
32
|
+
}
|
|
33
|
+
export async function readDocComments(command, door) {
|
|
34
|
+
const target = await resolveOrFail(command, door);
|
|
35
|
+
if ("exit" in target)
|
|
36
|
+
return target.exit;
|
|
37
|
+
const params = new URLSearchParams({ status: command.commentStatus ?? "open" });
|
|
38
|
+
if (command.cursor)
|
|
39
|
+
params.set("cursor", command.cursor);
|
|
40
|
+
const answer = await askAgentDoor(door, {
|
|
41
|
+
path: `/api/docs/documents/${encodeURIComponent(target.id)}/comments?${params.toString()}`,
|
|
42
|
+
method: "GET",
|
|
43
|
+
label: "docs comments",
|
|
44
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
45
|
+
});
|
|
46
|
+
if (!answer.ok)
|
|
47
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
48
|
+
const body = answer.body;
|
|
49
|
+
writeLine(door.io.stderr, `${TAG} read ${JSON.stringify({ document_id: target.id, status: body.status, threads: body.threads.length, coverage: body.coverage })}`);
|
|
50
|
+
if (door.json)
|
|
51
|
+
return emitAgentDoor(door, { ok: true, ...body });
|
|
52
|
+
writeLine(door.io.stdout, `${body.counts.open ?? "?"} open, ${body.counts.resolved ?? "?"} resolved. Showing ${body.status}.`);
|
|
53
|
+
for (const thread of body.threads) {
|
|
54
|
+
writeLine(door.io.stdout, `thread ${thread.id}${thread.resolved ? ` (resolved by ${thread.resolved_by_name ?? "someone"}, ${thread.resolved_at ?? ""})` : " (open)"}`);
|
|
55
|
+
for (const comment of thread.comments) {
|
|
56
|
+
if (comment.deleted && comment.body === null) {
|
|
57
|
+
writeLine(door.io.stdout, ` ${comment.id} ${comment.marker ?? "A comment was deleted."}`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
writeLine(door.io.stdout, ` ${comment.id} ${comment.author_name ?? "Someone"}, ${comment.created_at}${comment.edited_at ? " (edited)" : ""}${comment.deleted ? ` (${comment.marker ?? "deleted"})` : ""}`);
|
|
61
|
+
for (const line of (comment.body ?? "").split("\n"))
|
|
62
|
+
writeLine(door.io.stdout, ` ${line}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (body.coverage_note)
|
|
66
|
+
writeLine(door.io.stdout, body.coverage_note);
|
|
67
|
+
if (body.next_cursor)
|
|
68
|
+
writeLine(door.io.stdout, `More: --cursor ${body.next_cursor}`);
|
|
69
|
+
if (body.note)
|
|
70
|
+
writeLine(door.io.stdout, body.note);
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
async function readText(command, door) {
|
|
74
|
+
if (command.commentText !== "-")
|
|
75
|
+
return { ok: true, text: command.commentText ?? "" };
|
|
76
|
+
if (isInteractiveStdin(door.io))
|
|
77
|
+
return { ok: false, detail: "`-` reads the comment from stdin, but nothing is piped in." };
|
|
78
|
+
try {
|
|
79
|
+
return { ok: true, text: await readPipedText(door.io.stdin, { maxChars: TEXT_MAX_CHARS * 2, overflowMessage: `A comment is limited to ${TEXT_MAX_CHARS} characters.` }) };
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
return { ok: false, detail: error instanceof Error ? error.message : String(error) };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export async function commentDocs(command, door) {
|
|
86
|
+
const target = await resolveOrFail(command, door);
|
|
87
|
+
if ("exit" in target)
|
|
88
|
+
return target.exit;
|
|
89
|
+
const base = `/api/docs/documents/${encodeURIComponent(target.id)}`;
|
|
90
|
+
let text = "";
|
|
91
|
+
if (command.commentText !== undefined) {
|
|
92
|
+
const read = await readText(command, door);
|
|
93
|
+
if (!read.ok)
|
|
94
|
+
return failAgentDoor(door, TAG, "invalid_body", read.detail);
|
|
95
|
+
text = read.text;
|
|
96
|
+
}
|
|
97
|
+
let request;
|
|
98
|
+
if (command.commentDelete) {
|
|
99
|
+
request = { path: `${base}/comments/${command.commentDelete}`, method: "DELETE", label: "docs comment delete" };
|
|
100
|
+
}
|
|
101
|
+
else if (command.commentResolve || command.commentReopen) {
|
|
102
|
+
request = {
|
|
103
|
+
path: `${base}/threads/${command.commentResolve ?? command.commentReopen}`, method: "PATCH",
|
|
104
|
+
label: command.commentResolve ? "docs comment resolve" : "docs comment reopen", body: { resolved: Boolean(command.commentResolve) },
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
else if (command.commentEdit) {
|
|
108
|
+
request = { path: `${base}/comments/${command.commentEdit}`, method: "PATCH", label: "docs comment edit", body: { body: text } };
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
const requestId = command.requestId ?? randomUUID();
|
|
112
|
+
request = {
|
|
113
|
+
path: `${base}/comments`, method: "POST", label: command.commentReply ? "docs comment reply" : "docs comment",
|
|
114
|
+
body: { body: text, ...(command.commentReply ? { thread_id: command.commentReply } : {}), request_id: requestId }, requestId,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const answer = await askAgentDoor(door, { path: request.path, method: request.method, label: request.label, timeoutMs: WRITE_DEADLINE_MS, ...(request.body === undefined ? {} : { body: request.body }) });
|
|
118
|
+
if (!answer.ok)
|
|
119
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
120
|
+
const body = answer.body;
|
|
121
|
+
writeLine(door.io.stderr, `${TAG} ${request.label} ${JSON.stringify({ document_id: target.id, outcome: body.outcome, reason: body.reason, request_id: body.request_id ?? null })}`);
|
|
122
|
+
if (door.json)
|
|
123
|
+
return emitAgentDoor(door, { ok: true, ...body });
|
|
124
|
+
writeLine(door.io.stdout, body.outcome === "unchanged"
|
|
125
|
+
? `Nothing changed (${body.reason}).${body.comment_id ? ` Comment ${body.comment_id}.` : ""}`
|
|
126
|
+
: `Done (${request.label.replace("docs ", "")}).${body.comment_id ? ` Comment ${body.comment_id}` : ""}${body.thread_id ? ` in thread ${body.thread_id}` : ""}.`);
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit docs files|file` argument parsing (BLI-21786, docs parity step 9).
|
|
3
|
+
*
|
|
4
|
+
* `files <id|slug>` lists a page's files. `file upload <id|slug> <path> [--alt <text>]` reserves, PUTs and finishes;
|
|
5
|
+
* it prints the Markdown to paste and edits no body. `file show|remove <fileId>`; `file download <fileId> [--out <path>]`.
|
|
6
|
+
* The door checks authority and type; this file only checks shape.
|
|
7
|
+
*/
|
|
8
|
+
import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
9
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
10
|
+
const VALUE_FLAGS = ["--home", "--dashboard-url", "--alt", "--out"];
|
|
11
|
+
const VERBS = ["upload", "show", "download", "remove"];
|
|
12
|
+
export function parseDocsFilesArgs(args) {
|
|
13
|
+
const values = parseNamedArgs(args, { allowedFlags: [...VALUE_FLAGS, "--json"], valueFlags: VALUE_FLAGS });
|
|
14
|
+
const base = {
|
|
15
|
+
kind: "docs",
|
|
16
|
+
clearParent: false,
|
|
17
|
+
bodyStdin: false,
|
|
18
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
19
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
20
|
+
json: values.booleans.has("--json"),
|
|
21
|
+
};
|
|
22
|
+
const alt = optionalNonEmpty(values.flags.get("--alt"));
|
|
23
|
+
const out = optionalNonEmpty(values.flags.get("--out"));
|
|
24
|
+
if (args[0] === "files") {
|
|
25
|
+
const rest = values.positionals.slice(1);
|
|
26
|
+
if (alt !== undefined || out !== undefined)
|
|
27
|
+
throw new Error("--alt belongs to `cockpit docs file upload` and --out to `cockpit docs file download`.");
|
|
28
|
+
if (rest.length !== 1)
|
|
29
|
+
throw new Error("docs files needs exactly one page id or slug, e.g. `cockpit docs files runbook-x`.");
|
|
30
|
+
return { ...base, action: "files", docRef: rest[0] };
|
|
31
|
+
}
|
|
32
|
+
const verb = values.positionals[1];
|
|
33
|
+
const rest = values.positionals.slice(2);
|
|
34
|
+
if (!verb || !VERBS.includes(verb))
|
|
35
|
+
throw new Error(`docs file takes ${VERBS.join(", ")}, e.g. \`cockpit docs file upload <page> ./chart.png --alt "Q3 chart"\`.`);
|
|
36
|
+
if (alt !== undefined && verb !== "upload")
|
|
37
|
+
throw new Error("--alt belongs to `cockpit docs file upload`.");
|
|
38
|
+
if (out !== undefined && verb !== "download")
|
|
39
|
+
throw new Error("--out belongs to `cockpit docs file download`.");
|
|
40
|
+
if (verb === "upload") {
|
|
41
|
+
if (rest.length !== 2)
|
|
42
|
+
throw new Error("docs file upload needs a page (id or slug) and a file path: `cockpit docs file upload <page> <path> [--alt <text>]`.");
|
|
43
|
+
return { ...base, action: "file", docRef: rest[0], filePath: rest[1], docsFile: { verb, ...(alt ? { alt } : {}) } };
|
|
44
|
+
}
|
|
45
|
+
if (rest.length !== 1 || !UUID.test(rest[0] ?? "")) {
|
|
46
|
+
throw new Error(`docs file ${verb} needs one file id (a UUID, from \`cockpit docs files <page> --json\`).`);
|
|
47
|
+
}
|
|
48
|
+
return { ...base, action: "file", docsFile: { verb, fileId: rest[0].toLowerCase(), ...(out ? { outPath: out } : {}) } };
|
|
49
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit docs files|file` (BLI-21786): page files over the same doors the browser calls.
|
|
3
|
+
*
|
|
4
|
+
* files <page> GET /api/docs/documents/[id]/attachments
|
|
5
|
+
* file upload <page> <path> [--alt] POST /api/docs/documents/[id]/attachments, PUT to storage, finalize
|
|
6
|
+
* file show <fileId> GET /api/docs/attachments/[id]
|
|
7
|
+
* file download <fileId> [--out] GET /api/docs/attachments/[id]/download?format=json, then the signed link
|
|
8
|
+
* file remove <fileId> DELETE /api/docs/attachments/[id]
|
|
9
|
+
*
|
|
10
|
+
* The device token goes to Tower's own routes only; the PUT and the download go to the storage host with the
|
|
11
|
+
* signed URL and nothing else. Labels come back verbatim. File names are shown to the person, never written
|
|
12
|
+
* to a log line. Upload edits no body: it prints the Markdown to paste.
|
|
13
|
+
*/
|
|
14
|
+
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
15
|
+
import { basename, extname, resolve } from "node:path";
|
|
16
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
17
|
+
import { writeLine } from "./cli-io.js";
|
|
18
|
+
import { resolveDocId } from "./docs.js";
|
|
19
|
+
const TAG = "[docs files cli]";
|
|
20
|
+
const DEADLINE_MS = 30_000;
|
|
21
|
+
/** A 25 MB transfer on a slow link; the signed URL itself lives two hours. */
|
|
22
|
+
const TRANSFER_DEADLINE_MS = 10 * 60_000;
|
|
23
|
+
const MAX_BYTES = 26_214_400;
|
|
24
|
+
const MEDIA_TYPES = {
|
|
25
|
+
".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp",
|
|
26
|
+
".pdf": "application/pdf", ".txt": "text/plain", ".md": "text/markdown", ".json": "application/json",
|
|
27
|
+
".csv": "text/csv", ".zip": "application/zip", ".svg": "image/svg+xml",
|
|
28
|
+
};
|
|
29
|
+
const describe = (file) => `${file.id} ${file.state} ${file.byte_size} bytes ${file.name}`;
|
|
30
|
+
export async function runDocsFiles(command, door) {
|
|
31
|
+
if (command.action === "files")
|
|
32
|
+
return listFiles(command, door);
|
|
33
|
+
switch (command.docsFile?.verb) {
|
|
34
|
+
case "upload": return uploadFile(command, door);
|
|
35
|
+
case "show": return showFile(command, door);
|
|
36
|
+
case "download": return downloadFile(command, door);
|
|
37
|
+
case "remove": return removeFile(command, door);
|
|
38
|
+
default: return failAgentDoor(door, TAG, "invalid_body", "Try files, file upload, file show, file download or file remove.");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function pageId(command, door) {
|
|
42
|
+
const ref = command.docRef ?? "";
|
|
43
|
+
const resolved = await resolveDocId(door, ref);
|
|
44
|
+
if (resolved.status === "list_failed")
|
|
45
|
+
return failAgentDoor(door, TAG, resolved.reason, resolved.detail);
|
|
46
|
+
if (resolved.status === "not_found") {
|
|
47
|
+
return failAgentDoor(door, TAG, "document_not_found_or_unreadable", `That document does not exist, or you cannot read it: ${ref}`);
|
|
48
|
+
}
|
|
49
|
+
return resolved.id;
|
|
50
|
+
}
|
|
51
|
+
async function listFiles(command, door) {
|
|
52
|
+
const id = await pageId(command, door);
|
|
53
|
+
if (typeof id === "number")
|
|
54
|
+
return id;
|
|
55
|
+
const answer = await askAgentDoor(door, { path: `/api/docs/documents/${encodeURIComponent(id)}/attachments`, method: "GET", label: "docs files", timeoutMs: DEADLINE_MS });
|
|
56
|
+
if (!answer.ok)
|
|
57
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
58
|
+
const body = answer.body;
|
|
59
|
+
writeLine(door.io.stderr, `${TAG} list ${JSON.stringify({ document_id: id, returned: body.returned, coverage: body.coverage })}`);
|
|
60
|
+
if (door.json)
|
|
61
|
+
return emitAgentDoor(door, { ok: true, ...body });
|
|
62
|
+
if (body.files.length === 0)
|
|
63
|
+
writeLine(door.io.stdout, "This page has no files.");
|
|
64
|
+
for (const file of body.files)
|
|
65
|
+
writeLine(door.io.stdout, describe(file));
|
|
66
|
+
writeLine(door.io.stdout, `${body.returned} file(s), coverage ${body.coverage}. ${body.can_edit ? "You can upload and remove files here." : "You can read and download these files."}`);
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
async function uploadFile(command, door) {
|
|
70
|
+
const path = resolve(command.filePath ?? "");
|
|
71
|
+
let bytes;
|
|
72
|
+
try {
|
|
73
|
+
const info = await stat(path);
|
|
74
|
+
if (!info.isFile())
|
|
75
|
+
return failAgentDoor(door, TAG, "invalid_body", "That path is not a file.");
|
|
76
|
+
if (info.size > MAX_BYTES)
|
|
77
|
+
return failAgentDoor(door, TAG, "attachment_too_large", `A file can be at most ${MAX_BYTES.toLocaleString("en-US")} bytes (25 MB).`);
|
|
78
|
+
bytes = await readFile(path);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return failAgentDoor(door, TAG, "invalid_body", "Tower could not read that file. Check the path.");
|
|
82
|
+
}
|
|
83
|
+
const id = await pageId(command, door);
|
|
84
|
+
if (typeof id === "number")
|
|
85
|
+
return id;
|
|
86
|
+
const name = basename(path) || "file";
|
|
87
|
+
const mediaType = MEDIA_TYPES[extname(name).toLowerCase()];
|
|
88
|
+
const reserved = await askAgentDoor(door, {
|
|
89
|
+
path: `/api/docs/documents/${encodeURIComponent(id)}/attachments`, method: "POST", label: "docs file reserve", timeoutMs: DEADLINE_MS,
|
|
90
|
+
body: { name, ...(mediaType ? { media_type: mediaType } : {}), byte_size: bytes.length, ...(command.docsFile?.alt ? { alt_text: command.docsFile.alt } : {}) },
|
|
91
|
+
});
|
|
92
|
+
if (!reserved.ok)
|
|
93
|
+
return failAgentDoor(door, TAG, reserved.reason, reserved.detail);
|
|
94
|
+
const { file, upload } = reserved.body;
|
|
95
|
+
let put;
|
|
96
|
+
try {
|
|
97
|
+
put = await door.io.fetch(upload.url, {
|
|
98
|
+
method: upload.method ?? "PUT", headers: upload.headers ?? {}, body: new Uint8Array(bytes),
|
|
99
|
+
signal: AbortSignal.timeout(TRANSFER_DEADLINE_MS),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
writeLine(door.io.stderr, `${TAG} put failed ${JSON.stringify({ attachment_id: file.id, reason: "storage_unreachable", bytes: bytes.length, error_name: error instanceof Error ? error.name : "error" })}`);
|
|
104
|
+
return failAgentDoor(door, TAG, "storage_unreachable", `Could not reach storage. Nothing was sent. Cancel with \`cockpit docs file remove ${file.id}\` or upload again.`, { attachment_id: file.id });
|
|
105
|
+
}
|
|
106
|
+
if (!put.ok) {
|
|
107
|
+
writeLine(door.io.stderr, `${TAG} put refused ${JSON.stringify({ attachment_id: file.id, reason: `storage_http_${put.status}`, bytes: bytes.length })}`);
|
|
108
|
+
return failAgentDoor(door, TAG, "storage_unreachable", `Storage refused the upload (HTTP ${put.status}). Cancel with \`cockpit docs file remove ${file.id}\` or upload again.`, { attachment_id: file.id });
|
|
109
|
+
}
|
|
110
|
+
const done = await askAgentDoor(door, { path: `/api/docs/attachments/${file.id}/finalize`, method: "POST", label: "docs file finalize", timeoutMs: DEADLINE_MS, body: {} });
|
|
111
|
+
if (!done.ok) {
|
|
112
|
+
writeLine(door.io.stderr, `${TAG} finish failed, safe to repeat: POST /api/docs/attachments/${file.id}/finalize`);
|
|
113
|
+
return failAgentDoor(door, TAG, done.reason, done.detail, { attachment_id: file.id });
|
|
114
|
+
}
|
|
115
|
+
const ready = done.body.file;
|
|
116
|
+
writeLine(door.io.stderr, `${TAG} uploaded ${JSON.stringify({ attachment_id: ready.id, document_id: id, bytes: bytes.length, state: ready.state })}`);
|
|
117
|
+
if (door.json)
|
|
118
|
+
return emitAgentDoor(door, { ok: true, file: ready });
|
|
119
|
+
writeLine(door.io.stdout, `Uploaded ${describe(ready)}`);
|
|
120
|
+
writeLine(door.io.stdout, "Add this line to the page body (this command does not edit the body):");
|
|
121
|
+
writeLine(door.io.stdout, ready.markdown);
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
async function showFile(command, door) {
|
|
125
|
+
const answer = await askAgentDoor(door, { path: `/api/docs/attachments/${encodeURIComponent(command.docsFile?.fileId ?? "")}`, method: "GET", label: "docs file show", timeoutMs: DEADLINE_MS });
|
|
126
|
+
if (!answer.ok)
|
|
127
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
128
|
+
const file = answer.body.file;
|
|
129
|
+
if (door.json)
|
|
130
|
+
return emitAgentDoor(door, { ok: true, file });
|
|
131
|
+
writeLine(door.io.stdout, describe(file));
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
async function removeFile(command, door) {
|
|
135
|
+
const answer = await askAgentDoor(door, { path: `/api/docs/attachments/${encodeURIComponent(command.docsFile?.fileId ?? "")}`, method: "DELETE", label: "docs file remove", timeoutMs: DEADLINE_MS });
|
|
136
|
+
if (!answer.ok)
|
|
137
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
138
|
+
const file = answer.body.file;
|
|
139
|
+
writeLine(door.io.stderr, `${TAG} removed ${JSON.stringify({ attachment_id: file.id, state: file.state })}`);
|
|
140
|
+
if (door.json)
|
|
141
|
+
return emitAgentDoor(door, { ok: true, file });
|
|
142
|
+
writeLine(door.io.stdout, `${file.state === "cancelled" ? "Cancelled" : "Removed"} ${describe(file)}`);
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
async function downloadFile(command, door) {
|
|
146
|
+
const id = encodeURIComponent(command.docsFile?.fileId ?? "");
|
|
147
|
+
const described = await askAgentDoor(door, { path: `/api/docs/attachments/${id}`, method: "GET", label: "docs file show", timeoutMs: DEADLINE_MS });
|
|
148
|
+
if (!described.ok)
|
|
149
|
+
return failAgentDoor(door, TAG, described.reason, described.detail);
|
|
150
|
+
const file = described.body.file;
|
|
151
|
+
const link = await askAgentDoor(door, { path: `/api/docs/attachments/${id}/download?format=json`, method: "GET", label: "docs file link", timeoutMs: DEADLINE_MS });
|
|
152
|
+
if (!link.ok)
|
|
153
|
+
return failAgentDoor(door, TAG, link.reason, link.detail);
|
|
154
|
+
const { url } = link.body;
|
|
155
|
+
let bytes;
|
|
156
|
+
try {
|
|
157
|
+
const response = await door.io.fetch(url, { signal: AbortSignal.timeout(TRANSFER_DEADLINE_MS) });
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
writeLine(door.io.stderr, `${TAG} download refused ${JSON.stringify({ attachment_id: file.id, reason: `storage_http_${response.status}` })}`);
|
|
160
|
+
return failAgentDoor(door, TAG, "storage_unreachable", `Storage refused the download (HTTP ${response.status}).`);
|
|
161
|
+
}
|
|
162
|
+
bytes = Buffer.from(await response.arrayBuffer());
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return failAgentDoor(door, TAG, "storage_unreachable", "Could not reach storage. Try again.");
|
|
166
|
+
}
|
|
167
|
+
const target = resolve(command.docsFile?.outPath ?? (basename(file.name) || file.id));
|
|
168
|
+
try {
|
|
169
|
+
await writeFile(target, bytes, { flag: "wx" });
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return failAgentDoor(door, TAG, "out_exists", "Tower will not overwrite or could not write that path. Pick another with --out.");
|
|
173
|
+
}
|
|
174
|
+
writeLine(door.io.stderr, `${TAG} downloaded ${JSON.stringify({ attachment_id: file.id, bytes: bytes.length })}`);
|
|
175
|
+
if (door.json)
|
|
176
|
+
return emitAgentDoor(door, { ok: true, file, path: target, bytes: bytes.length });
|
|
177
|
+
writeLine(door.io.stdout, `Saved ${bytes.length} bytes to ${target}`);
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit docs formatting [--json]` (BLI-21814): the docs editor's formatting help over `GET /api/docs/formatting`,
|
|
3
|
+
* the same door the web page, MCP `docs_formatting` and JARVIS `readDocFormatting` use. It reads no document and
|
|
4
|
+
* changes nothing; to change a page's blocks, edit its Markdown with `docs update --expected-revision`.
|
|
5
|
+
*/
|
|
6
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
7
|
+
import { writeLine } from "./cli-io.js";
|
|
8
|
+
import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
9
|
+
const TAG = "[docs formatting cli]";
|
|
10
|
+
const DEADLINE_MS = 30_000;
|
|
11
|
+
export function parseDocsFormattingArgs(args) {
|
|
12
|
+
const values = parseNamedArgs(args, { allowedFlags: ["--home", "--dashboard-url", "--json"], valueFlags: ["--home", "--dashboard-url"] });
|
|
13
|
+
if (values.positionals.length !== 1)
|
|
14
|
+
throw new Error("docs formatting takes no arguments: `cockpit docs formatting [--json]`.");
|
|
15
|
+
return {
|
|
16
|
+
kind: "docs",
|
|
17
|
+
action: "formatting",
|
|
18
|
+
clearParent: false,
|
|
19
|
+
bodyStdin: false,
|
|
20
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
21
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
22
|
+
json: values.booleans.has("--json"),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export async function runDocsFormatting(_command, door) {
|
|
26
|
+
const answer = await askAgentDoor(door, { path: "/api/docs/formatting", method: "GET", label: "docs formatting", timeoutMs: DEADLINE_MS });
|
|
27
|
+
if (!answer.ok)
|
|
28
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
29
|
+
const help = answer.body.formatting;
|
|
30
|
+
writeLine(door.io.stderr, `${TAG} read ${JSON.stringify({ blocks: help.blocks.length })}`);
|
|
31
|
+
if (door.json)
|
|
32
|
+
return emitAgentDoor(door, { ok: true, formatting: help });
|
|
33
|
+
writeLine(door.io.stdout, "Blocks (write the Markdown; keys: Mod is Cmd on macOS, Ctrl on Windows):");
|
|
34
|
+
for (const block of help.blocks) {
|
|
35
|
+
const keys = Object.entries(block.keys).map(([id, key]) => `${id} ${key}`);
|
|
36
|
+
writeLine(door.io.stdout, `- ${block.type}: ${block.markdown.replace(/\n/g, " ⏎ ")}${block.note ? ` (${block.note})` : ""}${keys.length > 0 ? ` [${keys.join("; ")}]` : ""}`);
|
|
37
|
+
}
|
|
38
|
+
for (const [title, notes] of [["Kept as text", help.kept_as_text], ["Dropped on save", help.dropped], ["Read-only", help.read_only], ["Unsupported", help.unsupported]]) {
|
|
39
|
+
writeLine(door.io.stdout, `${title}:`);
|
|
40
|
+
for (const note of notes)
|
|
41
|
+
writeLine(door.io.stdout, `- ${note.name}: ${note.note}`);
|
|
42
|
+
}
|
|
43
|
+
writeLine(door.io.stdout, `Block menu: ${help.menu_keys}. Change blocks by editing the page's Markdown with \`cockpit docs update\`.`);
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit docs inbox|subscribe|unsubscribe|notifications` argument parsing (BLI-21741, docs parity step 8).
|
|
3
|
+
*
|
|
4
|
+
* `inbox [--unread] [--kind mentioned|commented|edited] [--before <cursor>] [--limit <n>]` reads the inbox.
|
|
5
|
+
* `inbox read <id>... | --all` and `inbox unread <id>...` mark items. `subscribe|unsubscribe <id|slug>` follow or
|
|
6
|
+
* stop following a page. `notifications [--mentions on|off] [--comments on|off] [--edits on|off]` reads the
|
|
7
|
+
* three docs notification kinds, or sets the ones named. The door checks authority; this file only checks shape.
|
|
8
|
+
*/
|
|
9
|
+
import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
10
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
11
|
+
const KINDS = ["mentioned", "commented", "edited"];
|
|
12
|
+
const NOTIFY_FLAGS = ["--mentions", "--comments", "--edits"];
|
|
13
|
+
const VALUE_FLAGS = ["--home", "--dashboard-url", "--kind", "--before", "--limit", ...NOTIFY_FLAGS];
|
|
14
|
+
function onOff(flag, raw) {
|
|
15
|
+
if (raw === undefined)
|
|
16
|
+
return undefined;
|
|
17
|
+
if (raw === "on")
|
|
18
|
+
return true;
|
|
19
|
+
if (raw === "off")
|
|
20
|
+
return false;
|
|
21
|
+
throw new Error(`${flag} takes on or off, not "${raw}".`);
|
|
22
|
+
}
|
|
23
|
+
export function parseDocsInboxArgs(args) {
|
|
24
|
+
const values = parseNamedArgs(args, { allowedFlags: [...VALUE_FLAGS, "--unread", "--all", "--json"], valueFlags: VALUE_FLAGS });
|
|
25
|
+
const verb = args[0];
|
|
26
|
+
const rest = values.positionals.slice(1);
|
|
27
|
+
const base = {
|
|
28
|
+
kind: "docs",
|
|
29
|
+
clearParent: false,
|
|
30
|
+
bodyStdin: false,
|
|
31
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
32
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
33
|
+
json: values.booleans.has("--json"),
|
|
34
|
+
};
|
|
35
|
+
const stray = (allowed) => {
|
|
36
|
+
const bad = [...values.flags.keys(), ...values.booleans].find((flag) => !allowed.includes(flag) && flag !== "--json" && flag !== "--home" && flag !== "--dashboard-url");
|
|
37
|
+
if (bad)
|
|
38
|
+
throw new Error(`${bad} does not belong to \`cockpit docs ${verb === "inbox" && rest[0] ? `inbox ${rest[0]}` : verb}\`.`);
|
|
39
|
+
};
|
|
40
|
+
if (verb === "subscribe" || verb === "unsubscribe") {
|
|
41
|
+
stray([]);
|
|
42
|
+
if (rest.length !== 1)
|
|
43
|
+
throw new Error(`docs ${verb} needs exactly one document id or slug.`);
|
|
44
|
+
return { ...base, action: verb, docRef: rest[0] };
|
|
45
|
+
}
|
|
46
|
+
if (verb === "notifications") {
|
|
47
|
+
stray([...NOTIFY_FLAGS]);
|
|
48
|
+
if (rest.length > 0)
|
|
49
|
+
throw new Error("docs notifications takes flags only: --mentions, --comments, --edits (on or off).");
|
|
50
|
+
const set = {
|
|
51
|
+
mentions: onOff("--mentions", values.flags.get("--mentions")),
|
|
52
|
+
comments: onOff("--comments", values.flags.get("--comments")),
|
|
53
|
+
edits: onOff("--edits", values.flags.get("--edits")),
|
|
54
|
+
};
|
|
55
|
+
const given = Object.fromEntries(Object.entries(set).filter(([, value]) => value !== undefined));
|
|
56
|
+
return { ...base, action: "notifications", ...(Object.keys(given).length > 0 ? { notifySet: given } : {}) };
|
|
57
|
+
}
|
|
58
|
+
// inbox
|
|
59
|
+
if (rest[0] === "read" || rest[0] === "unread") {
|
|
60
|
+
const marking = rest[0];
|
|
61
|
+
stray(["--all"]);
|
|
62
|
+
const ids = rest.slice(1);
|
|
63
|
+
const all = values.booleans.has("--all");
|
|
64
|
+
if (all && marking === "unread")
|
|
65
|
+
throw new Error("--all only marks items read. Name the ids to mark unread.");
|
|
66
|
+
if (all && ids.length > 0)
|
|
67
|
+
throw new Error("docs inbox read takes ids or --all, not both.");
|
|
68
|
+
if (!all && ids.length === 0)
|
|
69
|
+
throw new Error(`docs inbox ${marking} needs one or more item ids (from \`cockpit docs inbox --json\`)${marking === "read" ? ", or --all" : ""}.`);
|
|
70
|
+
const bad = ids.find((id) => !UUID.test(id));
|
|
71
|
+
if (bad)
|
|
72
|
+
throw new Error(`"${bad}" is not an item id (a UUID); take it from \`cockpit docs inbox --json\`.`);
|
|
73
|
+
return { ...base, action: "inbox-read", inboxRead: marking === "read", ...(all ? { inboxAll: true } : { inboxIds: ids }) };
|
|
74
|
+
}
|
|
75
|
+
stray(["--unread", "--kind", "--before", "--limit"]);
|
|
76
|
+
if (rest.length > 0)
|
|
77
|
+
throw new Error(`docs inbox does not know "${rest[0]}". Use \`inbox read <id>...\`, \`inbox unread <id>...\`, or flags.`);
|
|
78
|
+
const kind = optionalNonEmpty(values.flags.get("--kind"));
|
|
79
|
+
if (kind !== undefined && !KINDS.includes(kind))
|
|
80
|
+
throw new Error(`--kind takes ${KINDS.join(", ")}, not "${kind}".`);
|
|
81
|
+
const before = optionalNonEmpty(values.flags.get("--before"));
|
|
82
|
+
const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
|
|
83
|
+
if (limit !== undefined && limit > 100)
|
|
84
|
+
throw new Error("--limit takes a whole number from 1 to 100.");
|
|
85
|
+
return {
|
|
86
|
+
...base,
|
|
87
|
+
action: "inbox",
|
|
88
|
+
...(values.booleans.has("--unread") ? { inboxUnread: true } : {}),
|
|
89
|
+
...(kind ? { inboxKind: kind } : {}),
|
|
90
|
+
...(before ? { inboxBefore: before } : {}),
|
|
91
|
+
...(limit !== undefined ? { limit } : {}),
|
|
92
|
+
};
|
|
93
|
+
}
|