@bli-cockpit/cli 0.2.242 → 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 +222 -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-auth.js +1 -1
- package/dist/commands/local-help-commands-tower-ops.js +2 -0
- package/dist/commands/local-help-commands-tower.js +47 -31
- 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 +2 -2
- 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-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 +62 -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/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,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit msg context` (BLI-21829): the Tower docs, tickets, calendar events and meeting notes pointed at from one message.
|
|
3
|
+
* Same doors the browser calls (`/api/msg/messages/<id>/context`, `.../context/<link-id>`); `--json` prints the door's body
|
|
4
|
+
* unchanged so link ids, kinds and `state: "unavailable"` match HTTP. Every target is read again for YOU: one you cannot open
|
|
5
|
+
* shows as "Not available to you" with no title. A refusal keeps the door's reason: context_target_unavailable,
|
|
6
|
+
* context_not_author, context_already_linked, context_limit_reached, context_link_not_found.
|
|
7
|
+
*/
|
|
8
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
9
|
+
import { writeLine } from "./cli-io.js";
|
|
10
|
+
const TAG = "[msg cli]";
|
|
11
|
+
const DEADLINE_MS = 30_000;
|
|
12
|
+
function line(link) {
|
|
13
|
+
if (link.state === "unavailable")
|
|
14
|
+
return `${link.link_id} ${link.kind} Not available to you`;
|
|
15
|
+
const label = link.kind === "ticket" && link.identifier ? `${link.identifier} ${link.title ?? ""}`.trim() : (link.title ?? link.id);
|
|
16
|
+
return `${link.link_id} ${link.kind} ${label}${link.href ? ` ${link.href}` : ""}`;
|
|
17
|
+
}
|
|
18
|
+
export async function runMsgContext(command, door) {
|
|
19
|
+
const messageId = encodeURIComponent(command.targetId ?? "");
|
|
20
|
+
if (command.contextRemove) {
|
|
21
|
+
const answer = await askAgentDoor(door, {
|
|
22
|
+
path: `/api/msg/messages/${messageId}/context/${encodeURIComponent(command.contextRemove)}`,
|
|
23
|
+
method: "DELETE",
|
|
24
|
+
label: "msg context remove",
|
|
25
|
+
timeoutMs: DEADLINE_MS,
|
|
26
|
+
});
|
|
27
|
+
if (!answer.ok)
|
|
28
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
29
|
+
writeLine(door.io.stderr, `${TAG} context removed ${JSON.stringify({ message_id: command.targetId, link_id: command.contextRemove })}`);
|
|
30
|
+
if (door.json)
|
|
31
|
+
return emitAgentDoor(door, { ok: true, ...answer.body });
|
|
32
|
+
writeLine(door.io.stdout, `Removed context ${command.contextRemove} from ${command.targetId}.`);
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
if (command.contextAdd) {
|
|
36
|
+
const add = command.contextAdd;
|
|
37
|
+
const answer = await askAgentDoor(door, {
|
|
38
|
+
path: `/api/msg/messages/${messageId}/context`,
|
|
39
|
+
method: "POST",
|
|
40
|
+
body: { target_kind: add.kind, target_id: add.id, ...(add.occurrence ? { occurrence_start: add.occurrence } : {}) },
|
|
41
|
+
label: "msg context add",
|
|
42
|
+
timeoutMs: DEADLINE_MS,
|
|
43
|
+
});
|
|
44
|
+
if (!answer.ok)
|
|
45
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
46
|
+
const link = answer.body.link;
|
|
47
|
+
writeLine(door.io.stderr, `${TAG} context added ${JSON.stringify({ message_id: command.targetId, kind: add.kind, link_id: link?.link_id ?? null })}`);
|
|
48
|
+
if (door.json)
|
|
49
|
+
return emitAgentDoor(door, { ok: true, ...answer.body });
|
|
50
|
+
writeLine(door.io.stdout, `Added ${add.kind} context to ${command.targetId}${link ? `:\n${line(link)}` : "."}`);
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
const answer = await askAgentDoor(door, { path: `/api/msg/messages/${messageId}/context`, method: "GET", label: "msg context", timeoutMs: DEADLINE_MS });
|
|
54
|
+
if (!answer.ok)
|
|
55
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
56
|
+
const links = answer.body.links ?? [];
|
|
57
|
+
writeLine(door.io.stderr, `${TAG} context ${JSON.stringify({ message_id: command.targetId, links: links.length, unavailable: links.filter((link) => link.state === "unavailable").length })}`);
|
|
58
|
+
if (door.json)
|
|
59
|
+
return emitAgentDoor(door, { ok: true, ...answer.body });
|
|
60
|
+
for (const link of links)
|
|
61
|
+
writeLine(door.io.stdout, line(link));
|
|
62
|
+
if (links.length === 0)
|
|
63
|
+
writeLine(door.io.stdout, "No Tower context on this message.");
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** `cockpit msg dm <email>`: lookup-or-create a direct message (BLI-3749). Split out of `msg.ts` for the file-size ratchet. */
|
|
2
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
3
|
+
import { writeLine } from "./cli-io.js";
|
|
4
|
+
const TAG = "[msg cli]";
|
|
5
|
+
const WRITE_DEADLINE_MS = 30_000;
|
|
6
|
+
export async function openDm(command, door) {
|
|
7
|
+
const emails = command.dmEmails ?? (command.dmEmail ? [command.dmEmail] : []);
|
|
8
|
+
const email = emails.join(", ");
|
|
9
|
+
if (emails.length === 0)
|
|
10
|
+
return failAgentDoor(door, TAG, "invalid_body", "msg dm needs an email address.");
|
|
11
|
+
const answer = await askAgentDoor(door, {
|
|
12
|
+
path: "/api/msg/channels",
|
|
13
|
+
method: "POST",
|
|
14
|
+
label: "msg dm",
|
|
15
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
16
|
+
body: { dm_participant_emails: emails },
|
|
17
|
+
});
|
|
18
|
+
if (!answer.ok)
|
|
19
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
20
|
+
const channel = answer.body.channel ?? null;
|
|
21
|
+
writeLine(door.io.stderr, `${TAG} dm resolved ${JSON.stringify({ channel_id: channel?.id ?? null })}`);
|
|
22
|
+
if (door.json)
|
|
23
|
+
return emitAgentDoor(door, { ok: true, channel });
|
|
24
|
+
// Idempotent by contract (`get_or_create_dm`), so the wording says what is
|
|
25
|
+
// true either way rather than claiming a creation that may not have happened.
|
|
26
|
+
writeLine(door.io.stdout, `${emails.length > 1 ? "Group message" : "Direct message"} with ${email}: ${channel?.id ?? "?"}`);
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit msg file` (BLI-21521): first-party message files over the same doors the browser calls.
|
|
3
|
+
*
|
|
4
|
+
* upload <channel> <path|-> reserve (POST /api/msg/files), PUT the bytes to the signed URL, finalize
|
|
5
|
+
* show <id> the descriptor
|
|
6
|
+
* download <id> [--out] a 60-second signed link, fetched to disk (never overwrites)
|
|
7
|
+
* cancel <id> ends an unattached file
|
|
8
|
+
* list <channel> your own unsent uploads in that channel
|
|
9
|
+
*
|
|
10
|
+
* The device token goes to Tower's own routes only; the PUT and the download go to the storage host with
|
|
11
|
+
* the signed URL and nothing else. File names are shown to the person but never written to a log line.
|
|
12
|
+
*/
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
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 { isInteractiveStdin, writeLine } from "./cli-io.js";
|
|
18
|
+
const TAG = "[msg file cli]";
|
|
19
|
+
const DEADLINE_MS = 30_000;
|
|
20
|
+
/** A 25 MB transfer on a slow link; the signed URL itself lives two hours. */
|
|
21
|
+
const TRANSFER_DEADLINE_MS = 10 * 60_000;
|
|
22
|
+
const MAX_BYTES = 26_214_400;
|
|
23
|
+
const MEDIA_TYPES = {
|
|
24
|
+
".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp",
|
|
25
|
+
".pdf": "application/pdf", ".txt": "text/plain", ".md": "text/markdown", ".json": "application/json",
|
|
26
|
+
".csv": "text/csv", ".zip": "application/zip", ".svg": "image/svg+xml",
|
|
27
|
+
};
|
|
28
|
+
const describe = (file) => `${file.id} ${file.state} ${file.byte_size} bytes ${file.name}`;
|
|
29
|
+
async function readStdinBytes(door) {
|
|
30
|
+
const chunks = [];
|
|
31
|
+
let total = 0;
|
|
32
|
+
for await (const chunk of door.io.stdin) {
|
|
33
|
+
const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
34
|
+
total += buffer.length;
|
|
35
|
+
if (total > MAX_BYTES)
|
|
36
|
+
return "too_large";
|
|
37
|
+
chunks.push(buffer);
|
|
38
|
+
}
|
|
39
|
+
return Buffer.concat(chunks);
|
|
40
|
+
}
|
|
41
|
+
/** `channelId` is already resolved by `msg.ts` for `upload` and `list`. */
|
|
42
|
+
export async function runMsgFile(command, door, channelId) {
|
|
43
|
+
switch (command.fileVerb) {
|
|
44
|
+
case "upload": return uploadFile(command, door, channelId ?? "");
|
|
45
|
+
case "show": return showFile(command, door);
|
|
46
|
+
case "download": return downloadFile(command, door);
|
|
47
|
+
case "cancel": return cancelFile(command, door);
|
|
48
|
+
case "list": return listFiles(door, channelId ?? "");
|
|
49
|
+
default: return failAgentDoor(door, TAG, "invalid_body", "Try upload, show, download, cancel or list.");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function uploadFile(command, door, channelId) {
|
|
53
|
+
const source = command.filePath ?? "";
|
|
54
|
+
let bytes;
|
|
55
|
+
let name = command.fileName ?? basename(source);
|
|
56
|
+
if (source === "-") {
|
|
57
|
+
if (isInteractiveStdin(door.io))
|
|
58
|
+
return failAgentDoor(door, TAG, "nothing_piped", "Pipe the file in, e.g. `cat report.pdf | cockpit msg file upload general - --name report.pdf`.");
|
|
59
|
+
const read = await readStdinBytes(door);
|
|
60
|
+
if (read === "too_large")
|
|
61
|
+
return failAgentDoor(door, TAG, "file_too_large", `A file can be at most ${MAX_BYTES.toLocaleString("en-US")} bytes (25 MB).`);
|
|
62
|
+
bytes = read;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const path = resolve(source);
|
|
66
|
+
try {
|
|
67
|
+
const info = await stat(path);
|
|
68
|
+
if (!info.isFile())
|
|
69
|
+
return failAgentDoor(door, TAG, "invalid_body", "That path is not a file.");
|
|
70
|
+
if (info.size > MAX_BYTES)
|
|
71
|
+
return failAgentDoor(door, TAG, "file_too_large", `A file can be at most ${MAX_BYTES.toLocaleString("en-US")} bytes (25 MB).`);
|
|
72
|
+
bytes = await readFile(path);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return failAgentDoor(door, TAG, "invalid_body", "Tower could not read that file. Check the path.");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (name === "")
|
|
79
|
+
name = "file";
|
|
80
|
+
const mediaType = MEDIA_TYPES[extname(name).toLowerCase()] ?? "application/octet-stream";
|
|
81
|
+
const reserved = await askAgentDoor(door, {
|
|
82
|
+
path: "/api/msg/files", method: "POST", label: "msg file reserve", timeoutMs: DEADLINE_MS,
|
|
83
|
+
body: { channel_id: channelId, name, media_type: mediaType, byte_size: bytes.length },
|
|
84
|
+
});
|
|
85
|
+
if (!reserved.ok)
|
|
86
|
+
return failAgentDoor(door, TAG, reserved.reason, reserved.detail);
|
|
87
|
+
const { file, upload } = reserved.body;
|
|
88
|
+
let put;
|
|
89
|
+
try {
|
|
90
|
+
put = await door.io.fetch(upload.url, {
|
|
91
|
+
method: upload.method ?? "PUT", headers: upload.headers ?? {}, body: new Uint8Array(bytes),
|
|
92
|
+
signal: AbortSignal.timeout(TRANSFER_DEADLINE_MS),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
writeLine(door.io.stderr, `${TAG} put failed ${JSON.stringify({ file_id: file.id, reason: "storage_unreachable", bytes: bytes.length, error_name: error instanceof Error ? error.name : "error" })}`);
|
|
97
|
+
return failAgentDoor(door, TAG, "storage_unreachable", `Could not reach storage. Nothing was sent. Cancel with \`cockpit msg file cancel ${file.id}\` or upload again.`, { file_id: file.id });
|
|
98
|
+
}
|
|
99
|
+
if (!put.ok) {
|
|
100
|
+
writeLine(door.io.stderr, `${TAG} put refused ${JSON.stringify({ file_id: file.id, reason: `storage_http_${put.status}`, bytes: bytes.length })}`);
|
|
101
|
+
return failAgentDoor(door, TAG, "storage_unreachable", `Storage refused the upload (HTTP ${put.status}). Cancel with \`cockpit msg file cancel ${file.id}\` or upload again.`, { file_id: file.id });
|
|
102
|
+
}
|
|
103
|
+
const done = await askAgentDoor(door, { path: `/api/msg/files/${file.id}/finalize`, method: "POST", label: "msg file finalize", timeoutMs: DEADLINE_MS, body: {} });
|
|
104
|
+
if (!done.ok) {
|
|
105
|
+
writeLine(door.io.stderr, `${TAG} finish failed, safe to repeat: POST /api/msg/files/${file.id}/finalize`);
|
|
106
|
+
return failAgentDoor(door, TAG, done.reason, done.detail, { file_id: file.id });
|
|
107
|
+
}
|
|
108
|
+
const ready = done.body.file;
|
|
109
|
+
writeLine(door.io.stderr, `${TAG} uploaded ${JSON.stringify({ file_id: ready.id, bytes: bytes.length, state: ready.state })}`);
|
|
110
|
+
if (door.json)
|
|
111
|
+
return emitAgentDoor(door, { ok: true, file: ready });
|
|
112
|
+
writeLine(door.io.stdout, `Uploaded ${describe(ready)}`);
|
|
113
|
+
writeLine(door.io.stdout, `Send it with: cockpit msg send ${command.channelRef ?? "<channel>"} --file ${ready.id}`);
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
async function showFile(command, door) {
|
|
117
|
+
const answer = await askAgentDoor(door, { path: `/api/msg/files/${encodeURIComponent(command.targetId ?? "")}`, method: "GET", label: "msg file show", timeoutMs: DEADLINE_MS });
|
|
118
|
+
if (!answer.ok)
|
|
119
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
120
|
+
const file = answer.body.file;
|
|
121
|
+
if (door.json)
|
|
122
|
+
return emitAgentDoor(door, { ok: true, file });
|
|
123
|
+
writeLine(door.io.stdout, describe(file));
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
async function cancelFile(command, door) {
|
|
127
|
+
const answer = await askAgentDoor(door, { path: `/api/msg/files/${encodeURIComponent(command.targetId ?? "")}`, method: "DELETE", label: "msg file cancel", timeoutMs: DEADLINE_MS });
|
|
128
|
+
if (!answer.ok)
|
|
129
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
130
|
+
const file = answer.body.file;
|
|
131
|
+
writeLine(door.io.stderr, `${TAG} cancelled ${JSON.stringify({ file_id: file.id, state: file.state })}`);
|
|
132
|
+
if (door.json)
|
|
133
|
+
return emitAgentDoor(door, { ok: true, file });
|
|
134
|
+
writeLine(door.io.stdout, `Cancelled ${describe(file)}`);
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
async function listFiles(door, channelId) {
|
|
138
|
+
const answer = await askAgentDoor(door, { path: `/api/msg/files?channel_id=${encodeURIComponent(channelId)}`, method: "GET", label: "msg file list", timeoutMs: DEADLINE_MS });
|
|
139
|
+
if (!answer.ok)
|
|
140
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
141
|
+
const body = answer.body;
|
|
142
|
+
if (door.json)
|
|
143
|
+
return emitAgentDoor(door, { ok: true, ...body });
|
|
144
|
+
if (body.files.length === 0)
|
|
145
|
+
writeLine(door.io.stdout, "No unsent files in that channel.");
|
|
146
|
+
for (const file of body.files)
|
|
147
|
+
writeLine(door.io.stdout, describe(file));
|
|
148
|
+
writeLine(door.io.stdout, `${body.returned} unsent file(s), coverage ${body.coverage}.`);
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
async function downloadFile(command, door) {
|
|
152
|
+
const id = encodeURIComponent(command.targetId ?? "");
|
|
153
|
+
const described = await askAgentDoor(door, { path: `/api/msg/files/${id}`, method: "GET", label: "msg file show", timeoutMs: DEADLINE_MS });
|
|
154
|
+
if (!described.ok)
|
|
155
|
+
return failAgentDoor(door, TAG, described.reason, described.detail);
|
|
156
|
+
const file = described.body.file;
|
|
157
|
+
const link = await askAgentDoor(door, { path: `/api/msg/files/${id}/download?format=json`, method: "GET", label: "msg file link", timeoutMs: DEADLINE_MS });
|
|
158
|
+
if (!link.ok)
|
|
159
|
+
return failAgentDoor(door, TAG, link.reason, link.detail);
|
|
160
|
+
const { url } = link.body;
|
|
161
|
+
let bytes;
|
|
162
|
+
try {
|
|
163
|
+
const response = await door.io.fetch(url, { signal: AbortSignal.timeout(TRANSFER_DEADLINE_MS) });
|
|
164
|
+
if (!response.ok) {
|
|
165
|
+
writeLine(door.io.stderr, `${TAG} download refused ${JSON.stringify({ file_id: file.id, reason: `storage_http_${response.status}` })}`);
|
|
166
|
+
return failAgentDoor(door, TAG, "storage_unreachable", `Storage refused the download (HTTP ${response.status}).`);
|
|
167
|
+
}
|
|
168
|
+
bytes = Buffer.from(await response.arrayBuffer());
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return failAgentDoor(door, TAG, "storage_unreachable", "Could not reach storage. Try again.");
|
|
172
|
+
}
|
|
173
|
+
const target = resolve(command.outPath ?? basename(file.name) ?? randomUUID());
|
|
174
|
+
try {
|
|
175
|
+
await writeFile(target, bytes, { flag: "wx" });
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return failAgentDoor(door, TAG, "out_exists", "Tower will not overwrite or could not write that path. Pick another with --out.");
|
|
179
|
+
}
|
|
180
|
+
writeLine(door.io.stderr, `${TAG} downloaded ${JSON.stringify({ file_id: file.id, bytes: bytes.length })}`);
|
|
181
|
+
if (door.json)
|
|
182
|
+
return emitAgentDoor(door, { ok: true, file, path: target, bytes: bytes.length });
|
|
183
|
+
writeLine(door.io.stdout, `Saved ${bytes.length} bytes to ${target}`);
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arguments of `cockpit msg save|saved|pin|pins|forward` (BLI-21774): `save <message-id> [--remove]`, `saved [--before <cursor>]
|
|
3
|
+
* [--limit <n>]`, `pin <message-id> [--remove]`, `pins <channel>`, `forward <message-id> --to <channel> [--thread <id>]
|
|
4
|
+
* [--request-id <uuid>]` (the comment is piped in). Split out of `local-args-tower-msg.ts`.
|
|
5
|
+
*/
|
|
6
|
+
import { optionalNonEmpty, optionalPositiveInteger, optionalUrl } from "./local-arg-values.js";
|
|
7
|
+
export const MSG_MARK_ACTIONS = ["save", "saved", "pin", "pins", "forward"];
|
|
8
|
+
export const MSG_MARK_VALUE_FLAGS = ["--to"];
|
|
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
|
+
export function isMsgMarkAction(action) {
|
|
11
|
+
return MSG_MARK_ACTIONS.includes(action);
|
|
12
|
+
}
|
|
13
|
+
/** `--to` used on another msg verb is named, not ignored. */
|
|
14
|
+
export function rejectMsgMarkFlags(values) {
|
|
15
|
+
if (values.flags.has("--to"))
|
|
16
|
+
throw new Error("--to belongs to `cockpit msg forward`.");
|
|
17
|
+
}
|
|
18
|
+
export function parseMsgMarkArgs(action, values, rest) {
|
|
19
|
+
const stray = ["--channel", "--members", "--description", "--from", "--expected-revision", "--name", "--out", "--file", "--level", "--timezone", "--message"]
|
|
20
|
+
.find((flag) => values.flags.has(flag));
|
|
21
|
+
if (stray || values.booleans.has("--private"))
|
|
22
|
+
throw new Error(`${stray ?? "--private"} does not belong to \`cockpit msg ${action}\`.`);
|
|
23
|
+
const base = {
|
|
24
|
+
kind: "msg",
|
|
25
|
+
action,
|
|
26
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
27
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
28
|
+
isPrivate: false,
|
|
29
|
+
json: values.booleans.has("--json"),
|
|
30
|
+
};
|
|
31
|
+
const remove = values.booleans.has("--remove");
|
|
32
|
+
const before = optionalNonEmpty(values.flags.get("--before"));
|
|
33
|
+
const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
|
|
34
|
+
const thread = optionalNonEmpty(values.flags.get("--thread"));
|
|
35
|
+
const requestId = optionalNonEmpty(values.flags.get("--request-id"));
|
|
36
|
+
const to = optionalNonEmpty(values.flags.get("--to"));
|
|
37
|
+
const mentioned = ["--mentions", "--threads", "--unread", "--mark-read"].find((flag) => values.booleans.has(flag));
|
|
38
|
+
if (mentioned)
|
|
39
|
+
throw new Error(`${mentioned} does not belong to \`cockpit msg ${action}\`.`);
|
|
40
|
+
if (remove && action !== "save" && action !== "pin")
|
|
41
|
+
throw new Error("--remove belongs to `cockpit msg save` and `cockpit msg pin`.");
|
|
42
|
+
if ((before || limit !== undefined) && action !== "saved")
|
|
43
|
+
throw new Error("--before and --limit belong to `cockpit msg saved`.");
|
|
44
|
+
if ((thread || requestId || to) && action !== "forward")
|
|
45
|
+
throw new Error("--to, --thread and --request-id belong to `cockpit msg forward`.");
|
|
46
|
+
if (action === "saved") {
|
|
47
|
+
if (rest.length > 0)
|
|
48
|
+
throw new Error(`msg saved does not take "${rest[0]}".`);
|
|
49
|
+
return { ...base, ...(before ? { before } : {}), ...(limit === undefined ? {} : { limit }) };
|
|
50
|
+
}
|
|
51
|
+
if (action === "pins") {
|
|
52
|
+
const channelRef = optionalNonEmpty(rest[0]);
|
|
53
|
+
if (!channelRef)
|
|
54
|
+
throw new Error("msg pins needs a channel id or name, e.g. `cockpit msg pins general`.");
|
|
55
|
+
if (rest.length > 1)
|
|
56
|
+
throw new Error(`msg pins takes one channel, not ${rest.length}.`);
|
|
57
|
+
return { ...base, channelRef };
|
|
58
|
+
}
|
|
59
|
+
const targetId = optionalNonEmpty(rest[0]);
|
|
60
|
+
if (!targetId || !UUID.test(targetId)) {
|
|
61
|
+
throw new Error(`msg ${action} needs a message id (a UUID), e.g. \`cockpit msg ${action} <message-id>\`.`);
|
|
62
|
+
}
|
|
63
|
+
if (rest.length > 1)
|
|
64
|
+
throw new Error(`msg ${action} takes one message id, not ${rest.length}.`);
|
|
65
|
+
if (action === "forward") {
|
|
66
|
+
if (!to)
|
|
67
|
+
throw new Error("msg forward needs --to <channel>, e.g. `echo \"worth a look\" | cockpit msg forward <message-id> --to general`.");
|
|
68
|
+
if (thread && !UUID.test(thread))
|
|
69
|
+
throw new Error("--thread takes a message id (a UUID).");
|
|
70
|
+
if (requestId && !UUID.test(requestId))
|
|
71
|
+
throw new Error("--request-id takes a UUID.");
|
|
72
|
+
return {
|
|
73
|
+
...base, targetId, forwardTo: to,
|
|
74
|
+
...(thread ? { threadId: thread } : {}), ...(requestId ? { requestId } : {}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return { ...base, targetId, ...(remove ? { remove } : {}) };
|
|
78
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The saved, pin and forward verbs of `cockpit msg` (BLI-21774): `save`, `saved`, `pin`, `pins`, `forward`. Same doors the
|
|
3
|
+
* browser calls (`/api/msg/messages/<id>/save`, `/api/msg/saved`, `.../pin`, `/api/msg/channels/<id>/pins`,
|
|
4
|
+
* `.../forward`); `--json` prints the door's body unchanged so ids, rows, coverage and reasons match HTTP. A save is private
|
|
5
|
+
* to you; a pin is shared channel state; a forward posts ONE message holding your comment and the source's exact link,
|
|
6
|
+
* never its text. A refusal keeps the door's reason: message_not_found_or_unreadable, pin_limit_reached, pin_deleted_message,
|
|
7
|
+
* channel_archived, forward_source_deleted.
|
|
8
|
+
*/
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
11
|
+
import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
|
|
12
|
+
import { runMsgContext } from "./msg-context.js";
|
|
13
|
+
import { runMsgSchedule } from "./msg-schedule.js";
|
|
14
|
+
import { refuseMsgWrite } from "./msg-write.js";
|
|
15
|
+
const TAG = "[msg cli]";
|
|
16
|
+
const DEADLINE_MS = 30_000;
|
|
17
|
+
const COMMENT_MAX_CHARS = 2_000;
|
|
18
|
+
function oneLine(content, deleted) {
|
|
19
|
+
if (deleted)
|
|
20
|
+
return "(This message was deleted)";
|
|
21
|
+
const line = (content ?? "").split("\n")[0] ?? "";
|
|
22
|
+
return line.length > 140 ? `${line.slice(0, 137)}...` : line;
|
|
23
|
+
}
|
|
24
|
+
async function markVerb(command, door, kind) {
|
|
25
|
+
const on = command.remove !== true;
|
|
26
|
+
const answer = await askAgentDoor(door, {
|
|
27
|
+
path: `/api/msg/messages/${encodeURIComponent(command.targetId ?? "")}/${kind}`,
|
|
28
|
+
method: on ? "PUT" : "DELETE",
|
|
29
|
+
label: `msg ${kind}`,
|
|
30
|
+
timeoutMs: DEADLINE_MS,
|
|
31
|
+
});
|
|
32
|
+
if (!answer.ok)
|
|
33
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
34
|
+
const body = answer.body;
|
|
35
|
+
writeLine(door.io.stderr, `${TAG} ${kind} ${JSON.stringify({ message_id: command.targetId, on, changed: body.changed === true })}`);
|
|
36
|
+
if (door.json)
|
|
37
|
+
return emitAgentDoor(door, { ok: true, ...answer.body });
|
|
38
|
+
const past = kind === "save" ? (on ? "Saved" : "Unsaved") : on ? "Pinned" : "Unpinned";
|
|
39
|
+
writeLine(door.io.stdout, body.changed === false ? `${past} already: ${command.targetId} was already ${past.toLowerCase()}.` : `${past} ${command.targetId}.`);
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
async function savedVerb(command, door) {
|
|
43
|
+
const query = new URLSearchParams();
|
|
44
|
+
if (command.before)
|
|
45
|
+
query.set("before", command.before);
|
|
46
|
+
if (command.limit)
|
|
47
|
+
query.set("limit", String(command.limit));
|
|
48
|
+
const answer = await askAgentDoor(door, {
|
|
49
|
+
path: `/api/msg/saved${query.size > 0 ? `?${query.toString()}` : ""}`,
|
|
50
|
+
method: "GET",
|
|
51
|
+
label: "msg saved",
|
|
52
|
+
timeoutMs: DEADLINE_MS,
|
|
53
|
+
});
|
|
54
|
+
if (!answer.ok)
|
|
55
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
56
|
+
const body = answer.body;
|
|
57
|
+
const items = body.items ?? [];
|
|
58
|
+
writeLine(door.io.stderr, `${TAG} saved ${JSON.stringify({ returned: items.length, dropped: body.coverage?.dropped ?? 0, has_more: body.hasMore === true })}`);
|
|
59
|
+
if (door.json)
|
|
60
|
+
return emitAgentDoor(door, { ok: true, ...answer.body });
|
|
61
|
+
for (const item of items) {
|
|
62
|
+
writeLine(door.io.stdout, `${item.message_id} ${item.saved_at} ${oneLine(item.message.content, Boolean(item.message.deleted_at))}`);
|
|
63
|
+
}
|
|
64
|
+
if (items.length === 0)
|
|
65
|
+
writeLine(door.io.stdout, "Nothing saved yet.");
|
|
66
|
+
const dropped = body.coverage?.dropped ?? 0;
|
|
67
|
+
if (dropped > 0)
|
|
68
|
+
writeLine(door.io.stdout, `${dropped} saved ${dropped === 1 ? "message is" : "messages are"} not shown: you can no longer read ${dropped === 1 ? "it" : "them"}.`);
|
|
69
|
+
writeLine(door.io.stdout, body.hasMore && body.nextCursor ? `more: --before ${body.nextCursor}` : "complete");
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
async function pinsVerb(command, door, resolve) {
|
|
73
|
+
const ref = command.channelRef ?? "";
|
|
74
|
+
const resolved = await resolve(ref);
|
|
75
|
+
if (resolved.status === "list_failed")
|
|
76
|
+
return failAgentDoor(door, TAG, resolved.reason, resolved.detail);
|
|
77
|
+
if (resolved.status === "not_found") {
|
|
78
|
+
return failAgentDoor(door, TAG, "channel_not_found_or_unreadable", `That channel does not exist, or you cannot read it: ${ref}`);
|
|
79
|
+
}
|
|
80
|
+
const answer = await askAgentDoor(door, { path: `/api/msg/channels/${encodeURIComponent(resolved.id)}/pins`, method: "GET", label: "msg pins", timeoutMs: DEADLINE_MS });
|
|
81
|
+
if (!answer.ok)
|
|
82
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
83
|
+
const body = answer.body;
|
|
84
|
+
const pins = body.pins ?? [];
|
|
85
|
+
writeLine(door.io.stderr, `${TAG} pins ${JSON.stringify({ channel_id: resolved.id, returned: pins.length, dropped: body.coverage?.dropped ?? 0 })}`);
|
|
86
|
+
if (door.json)
|
|
87
|
+
return emitAgentDoor(door, { ok: true, ...answer.body });
|
|
88
|
+
for (const pin of pins) {
|
|
89
|
+
const who = pin.pinned_by ? body.displayNameById?.[pin.pinned_by] ?? "someone" : "someone";
|
|
90
|
+
writeLine(door.io.stdout, `${pin.message_id} pinned ${pin.pinned_at} by ${who} ${oneLine(pin.message.content, Boolean(pin.message.deleted_at))}`);
|
|
91
|
+
}
|
|
92
|
+
if (pins.length === 0)
|
|
93
|
+
writeLine(door.io.stdout, "Nothing is pinned in this channel.");
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
async function forwardVerb(command, door, resolve) {
|
|
97
|
+
const ref = command.forwardTo ?? "";
|
|
98
|
+
const resolved = await resolve(ref);
|
|
99
|
+
if (resolved.status === "list_failed")
|
|
100
|
+
return failAgentDoor(door, TAG, resolved.reason, resolved.detail);
|
|
101
|
+
if (resolved.status === "not_found") {
|
|
102
|
+
return failAgentDoor(door, TAG, "channel_not_found_or_unreadable", `That channel does not exist, or you cannot read it: ${ref}`);
|
|
103
|
+
}
|
|
104
|
+
let comment = "";
|
|
105
|
+
if (!isInteractiveStdin(door.io)) {
|
|
106
|
+
try {
|
|
107
|
+
comment = (await readPipedText(door.io.stdin, { maxChars: COMMENT_MAX_CHARS, overflowMessage: `A forward comment is limited to ${COMMENT_MAX_CHARS} characters.` })).trim();
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
return failAgentDoor(door, TAG, "content_too_long", error instanceof Error ? error.message : String(error));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const requestId = command.requestId ?? randomUUID();
|
|
114
|
+
const answer = await askAgentDoor(door, {
|
|
115
|
+
path: `/api/msg/messages/${encodeURIComponent(command.targetId ?? "")}/forward`,
|
|
116
|
+
method: "POST",
|
|
117
|
+
label: "msg forward",
|
|
118
|
+
timeoutMs: DEADLINE_MS,
|
|
119
|
+
body: {
|
|
120
|
+
channel_id: resolved.id,
|
|
121
|
+
request_id: requestId,
|
|
122
|
+
...(comment ? { comment } : {}),
|
|
123
|
+
...(command.threadId ? { thread_parent_id: command.threadId } : {}),
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
if (!answer.ok)
|
|
127
|
+
return refuseMsgWrite(door, answer, requestId, "forward");
|
|
128
|
+
const body = answer.body;
|
|
129
|
+
writeLine(door.io.stderr, `${TAG} forwarded ${JSON.stringify({ source_id: command.targetId, destination_channel_id: resolved.id, message_id: body.message?.id ?? null, request_id: requestId })}`);
|
|
130
|
+
if (door.json)
|
|
131
|
+
return emitAgentDoor(door, { ok: true, ...answer.body });
|
|
132
|
+
writeLine(door.io.stdout, `Forwarded ${command.targetId} to ${ref} as ${body.message?.id ?? "a new message"}. It holds your comment and the link only.`);
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
export async function runMsgMark(command, door, resolve) {
|
|
136
|
+
switch (command.action) {
|
|
137
|
+
case "save": return markVerb(command, door, "save");
|
|
138
|
+
case "pin": return markVerb(command, door, "pin");
|
|
139
|
+
case "saved": return savedVerb(command, door);
|
|
140
|
+
case "pins": return pinsVerb(command, door, resolve);
|
|
141
|
+
// BLI-21802: the schedule verbs ride the same entry so `msg.ts` stays under the file-size ratchet.
|
|
142
|
+
case "schedule":
|
|
143
|
+
case "remind":
|
|
144
|
+
case "scheduled": return runMsgSchedule(command, door, resolve);
|
|
145
|
+
case "context": return runMsgContext(command, door);
|
|
146
|
+
default: return forwardVerb(command, door, resolve);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arguments of `cockpit msg notify` (BLI-21605): `notify [--json]`, `notify set --level <all|mentions|none>
|
|
3
|
+
* [--channel <id|name>] [--mute|--unmute] [--clear]`, `notify quiet <HH:MM-HH:MM> [--timezone <zone>] | --off`,
|
|
4
|
+
* `notify receipts [--message <id>] [--before <cursor>] [--limit <n>]`. Split out of `local-args-tower-msg.ts`.
|
|
5
|
+
*/
|
|
6
|
+
import { optionalNonEmpty, optionalPositiveInteger, optionalUrl } from "./local-arg-values.js";
|
|
7
|
+
export const MSG_NOTIFY_ONLY_FLAGS = ["--level", "--timezone", "--message", "--mute", "--unmute", "--clear", "--off"];
|
|
8
|
+
const VERBS = new Set(["set", "quiet", "receipts"]);
|
|
9
|
+
const LEVELS = ["all", "mentions", "none"];
|
|
10
|
+
const WINDOW = /^([01]\d|2[0-3]):([0-5]\d)-([01]\d|2[0-3]):([0-5]\d)$/;
|
|
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
|
+
function browserZone() {
|
|
13
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
14
|
+
}
|
|
15
|
+
export function parseMsgNotifyArgs(values, rest) {
|
|
16
|
+
const first = rest[0];
|
|
17
|
+
if (first !== undefined && !VERBS.has(first)) {
|
|
18
|
+
throw new Error(`Unknown msg notify command: ${first}. Try set, quiet or receipts, or no verb to show your settings.`);
|
|
19
|
+
}
|
|
20
|
+
const verb = first ?? "show";
|
|
21
|
+
const positional = rest.slice(1);
|
|
22
|
+
const has = (flag) => values.flags.has(flag) || values.booleans.has(flag);
|
|
23
|
+
const allowed = {
|
|
24
|
+
show: [],
|
|
25
|
+
set: ["--level", "--channel", "--mute", "--unmute", "--clear"],
|
|
26
|
+
quiet: ["--timezone", "--off"],
|
|
27
|
+
receipts: ["--message", "--before", "--limit"],
|
|
28
|
+
};
|
|
29
|
+
const stray = [...MSG_NOTIFY_ONLY_FLAGS, "--channel", "--before", "--limit"].find((flag) => has(flag) && !allowed[verb].includes(flag));
|
|
30
|
+
if (stray)
|
|
31
|
+
throw new Error(`${stray} does not belong to \`cockpit msg notify${verb === "show" ? "" : ` ${verb}`}\`.`);
|
|
32
|
+
const stray2 = ["--thread", "--members", "--description", "--from", "--request-id", "--expected-revision", "--name", "--out", "--file", "--mark-read", "--mentions", "--threads", "--unread", "--remove", "--private"].find(has);
|
|
33
|
+
if (stray2)
|
|
34
|
+
throw new Error(`${stray2} does not belong to \`cockpit msg notify\`.`);
|
|
35
|
+
const level = optionalNonEmpty(values.flags.get("--level"));
|
|
36
|
+
const channelRef = optionalNonEmpty(values.flags.get("--channel"));
|
|
37
|
+
const mute = values.booleans.has("--mute");
|
|
38
|
+
const unmute = values.booleans.has("--unmute");
|
|
39
|
+
const clear = values.booleans.has("--clear");
|
|
40
|
+
const quietOff = values.booleans.has("--off");
|
|
41
|
+
const timezone = optionalNonEmpty(values.flags.get("--timezone"));
|
|
42
|
+
const messageId = optionalNonEmpty(values.flags.get("--message"));
|
|
43
|
+
const before = optionalNonEmpty(values.flags.get("--before"));
|
|
44
|
+
const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
|
|
45
|
+
let quietWindow;
|
|
46
|
+
if (verb === "set") {
|
|
47
|
+
if (positional.length > 0)
|
|
48
|
+
throw new Error(`msg notify set does not take "${positional[0]}".`);
|
|
49
|
+
if (level && !LEVELS.includes(level))
|
|
50
|
+
throw new Error("--level must be all, mentions or none.");
|
|
51
|
+
if (mute && unmute)
|
|
52
|
+
throw new Error("Pick one of --mute or --unmute.");
|
|
53
|
+
if (!channelRef && (mute || unmute || clear))
|
|
54
|
+
throw new Error("--mute, --unmute and --clear need --channel <id|name>.");
|
|
55
|
+
if (clear && (level || mute || unmute))
|
|
56
|
+
throw new Error("--clear removes the channel override; do not combine it with --level, --mute or --unmute.");
|
|
57
|
+
if (!level && !mute && !unmute && !clear)
|
|
58
|
+
throw new Error("msg notify set needs --level <all|mentions|none>, or with --channel also --mute, --unmute or --clear.");
|
|
59
|
+
}
|
|
60
|
+
if (verb === "quiet") {
|
|
61
|
+
quietWindow = optionalNonEmpty(positional[0]);
|
|
62
|
+
if (quietOff && quietWindow)
|
|
63
|
+
throw new Error("Pass a window or --off, not both.");
|
|
64
|
+
if (!quietOff && (!quietWindow || !WINDOW.test(quietWindow)))
|
|
65
|
+
throw new Error("msg notify quiet needs a window like 22:00-07:00 (24-hour, may wrap midnight), or --off.");
|
|
66
|
+
if (positional.length > 1)
|
|
67
|
+
throw new Error("msg notify quiet takes one window.");
|
|
68
|
+
if (quietOff && timezone)
|
|
69
|
+
throw new Error("--timezone belongs with a window, not --off.");
|
|
70
|
+
if (quietWindow && quietWindow.slice(0, 5) === quietWindow.slice(6))
|
|
71
|
+
throw new Error("Quiet hours need a start and an end that differ.");
|
|
72
|
+
}
|
|
73
|
+
if (verb === "receipts") {
|
|
74
|
+
if (positional.length > 0)
|
|
75
|
+
throw new Error(`msg notify receipts does not take "${positional[0]}".`);
|
|
76
|
+
if (messageId && !UUID.test(messageId))
|
|
77
|
+
throw new Error("--message takes a message id (a UUID).");
|
|
78
|
+
if (limit !== undefined && limit > 100)
|
|
79
|
+
throw new Error("--limit takes 1 to 100.");
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
kind: "msg",
|
|
83
|
+
action: "notify",
|
|
84
|
+
notifyVerb: verb,
|
|
85
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
86
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
87
|
+
isPrivate: false,
|
|
88
|
+
json: values.booleans.has("--json"),
|
|
89
|
+
...(level ? { notifyLevel: level } : {}),
|
|
90
|
+
...(channelRef ? { channelRef } : {}),
|
|
91
|
+
...(mute ? { mute } : {}),
|
|
92
|
+
...(unmute ? { unmute } : {}),
|
|
93
|
+
...(clear ? { clear } : {}),
|
|
94
|
+
...(quietOff ? { quietOff } : {}),
|
|
95
|
+
...(quietWindow ? { quietWindow, timezone: timezone ?? browserZone() } : {}),
|
|
96
|
+
...(messageId ? { targetId: messageId } : {}),
|
|
97
|
+
...(before ? { before } : {}),
|
|
98
|
+
...(limit === undefined ? {} : { limit }),
|
|
99
|
+
};
|
|
100
|
+
}
|