@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,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The schedule verbs of `cockpit msg` (BLI-21802): `schedule`, `remind`, `scheduled` (list), `scheduled change|cancel|done`.
|
|
3
|
+
* Same doors the browser calls (`/api/msg/schedules`, `.../<id>`, `.../<id>/done`); `--json` prints the door's body unchanged so
|
|
4
|
+
* ids, states, instants and reasons match HTTP, the web page, MCP and JARVIS. `--at` is an ISO instant with an offset, or a
|
|
5
|
+
* wall time "YYYY-MM-DD HH:MM" in `--timezone` (this machine's zone when none is given, and the answer names it). A schedule
|
|
6
|
+
* is yours alone. The message body is piped in and never travels on the command line. A refusal keeps the door's reason:
|
|
7
|
+
* invalid_schedule_time, invalid_timezone, schedule_limit_reached, schedule_not_found, schedule_already_sent,
|
|
8
|
+
* schedule_revision_conflict, request_id_reused.
|
|
9
|
+
*/
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
12
|
+
import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
|
|
13
|
+
const TAG = "[msg cli]";
|
|
14
|
+
const DEADLINE_MS = 30_000;
|
|
15
|
+
const BODY_MAX_CHARS = 8_000;
|
|
16
|
+
const LOCAL_WALL = /^\d{4}-\d\d-\d\d[ T]\d\d:\d\d$/;
|
|
17
|
+
function systemZone() {
|
|
18
|
+
try {
|
|
19
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return "UTC";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** The `at` field of a door body, plus the zone the answer should name when the person gave a wall time without one. */
|
|
26
|
+
function atField(command) {
|
|
27
|
+
const raw = (command.scheduleAt ?? "").trim();
|
|
28
|
+
if (LOCAL_WALL.test(raw)) {
|
|
29
|
+
const assumedZone = !command.timezone;
|
|
30
|
+
const zone = command.timezone ?? systemZone();
|
|
31
|
+
return { at: { local: raw, timezone: zone }, zone, assumedZone };
|
|
32
|
+
}
|
|
33
|
+
return { at: raw, zone: command.timezone ?? null, assumedZone: false };
|
|
34
|
+
}
|
|
35
|
+
function oneLine(text) {
|
|
36
|
+
const line = (text ?? "").split("\n")[0] ?? "";
|
|
37
|
+
return line.length > 100 ? `${line.slice(0, 97)}...` : line;
|
|
38
|
+
}
|
|
39
|
+
function describe(row) {
|
|
40
|
+
return `${row.id} ${row.state} ${row.kind} ${row.due_at} (${row.timezone}) rev ${row.revision}${row.reason ? ` ${row.reason}` : ""}`;
|
|
41
|
+
}
|
|
42
|
+
/** A write whose answer may have been lost keeps its request id: the same input with the same id stores once. */
|
|
43
|
+
function refuseWrite(door, answer, requestId) {
|
|
44
|
+
const mutation = answer.body?.mutation;
|
|
45
|
+
if (answer.httpStatus === undefined || answer.httpStatus >= 500) {
|
|
46
|
+
writeLine(door.io.stderr, `${TAG} the result is unknown. List with \`cockpit msg scheduled\`, or retry unchanged input with --request-id ${requestId}; it stores once.`);
|
|
47
|
+
}
|
|
48
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail, mutation ? { mutation } : {});
|
|
49
|
+
}
|
|
50
|
+
async function readBody(command, door, required) {
|
|
51
|
+
if (isInteractiveStdin(door.io)) {
|
|
52
|
+
if (!required)
|
|
53
|
+
return { ok: true, text: "" };
|
|
54
|
+
return { ok: false, code: failAgentDoor(door, TAG, "nothing_piped", 'Pipe the message in, e.g. `echo "hello" | cockpit msg schedule ops --at "2026-11-02 09:00"`. A message never travels on the command line.') };
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const text = await readPipedText(door.io.stdin, { maxChars: BODY_MAX_CHARS, overflowMessage: `A scheduled message is limited to ${BODY_MAX_CHARS} characters.` });
|
|
58
|
+
return { ok: true, text: text.trim() };
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
void command;
|
|
62
|
+
return { ok: false, code: failAgentDoor(door, TAG, "content_too_long", error instanceof Error ? error.message : String(error)) };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function sayAssumedZone(door, zone, assumed) {
|
|
66
|
+
if (assumed && zone)
|
|
67
|
+
writeLine(door.io.stderr, `${TAG} no --timezone given; read that wall time in this machine's zone, ${zone}.`);
|
|
68
|
+
}
|
|
69
|
+
async function scheduleVerb(command, door, resolve) {
|
|
70
|
+
const ref = command.channelRef ?? "";
|
|
71
|
+
const resolved = await resolve(ref);
|
|
72
|
+
if (resolved.status === "list_failed")
|
|
73
|
+
return failAgentDoor(door, TAG, resolved.reason, resolved.detail);
|
|
74
|
+
if (resolved.status === "not_found") {
|
|
75
|
+
return failAgentDoor(door, TAG, "channel_not_found_or_unreadable", `That channel does not exist, or you cannot read it: ${ref}`);
|
|
76
|
+
}
|
|
77
|
+
const body = await readBody(command, door, true);
|
|
78
|
+
if (!body.ok)
|
|
79
|
+
return body.code;
|
|
80
|
+
if (body.text === "")
|
|
81
|
+
return failAgentDoor(door, TAG, "content_too_long", "Nothing was piped in. A scheduled message is 1 to 8000 characters.");
|
|
82
|
+
const when = atField(command);
|
|
83
|
+
sayAssumedZone(door, when.zone, when.assumedZone);
|
|
84
|
+
const requestId = command.requestId ?? randomUUID();
|
|
85
|
+
const answer = await askAgentDoor(door, {
|
|
86
|
+
path: "/api/msg/schedules",
|
|
87
|
+
method: "POST",
|
|
88
|
+
label: "msg schedule",
|
|
89
|
+
timeoutMs: DEADLINE_MS,
|
|
90
|
+
body: {
|
|
91
|
+
kind: "message", channel_id: resolved.id, body: body.text, at: when.at, request_id: requestId,
|
|
92
|
+
...(when.zone && typeof when.at === "string" ? { timezone: when.zone } : {}),
|
|
93
|
+
...(command.threadId ? { thread_parent_id: command.threadId } : {}),
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
if (!answer.ok)
|
|
97
|
+
return refuseWrite(door, answer, requestId);
|
|
98
|
+
const stored = answer.body.schedule;
|
|
99
|
+
writeLine(door.io.stderr, `${TAG} scheduled ${JSON.stringify({ schedule_id: stored?.id ?? null, channel_id: resolved.id, request_id: requestId, replayed: answer.body.replayed === true })}`);
|
|
100
|
+
if (door.json)
|
|
101
|
+
return emitAgentDoor(door, { ok: true, ...answer.body });
|
|
102
|
+
writeLine(door.io.stdout, `Scheduled ${stored?.id ?? "a message"} for ${stored?.due_at ?? command.scheduleAt} (${stored?.timezone ?? when.zone ?? "UTC"}) in ${ref}. List: cockpit msg scheduled.`);
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
async function remindVerb(command, door) {
|
|
106
|
+
const when = atField(command);
|
|
107
|
+
sayAssumedZone(door, when.zone, when.assumedZone);
|
|
108
|
+
const requestId = command.requestId ?? randomUUID();
|
|
109
|
+
const answer = await askAgentDoor(door, {
|
|
110
|
+
path: "/api/msg/schedules",
|
|
111
|
+
method: "POST",
|
|
112
|
+
label: "msg remind",
|
|
113
|
+
timeoutMs: DEADLINE_MS,
|
|
114
|
+
body: {
|
|
115
|
+
kind: "reminder", message_id: command.targetId, at: when.at, request_id: requestId,
|
|
116
|
+
...(when.zone && typeof when.at === "string" ? { timezone: when.zone } : {}),
|
|
117
|
+
...(command.scheduleNote ? { note: command.scheduleNote } : {}),
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
if (!answer.ok)
|
|
121
|
+
return refuseWrite(door, answer, requestId);
|
|
122
|
+
const stored = answer.body.schedule;
|
|
123
|
+
writeLine(door.io.stderr, `${TAG} reminder ${JSON.stringify({ schedule_id: stored?.id ?? null, message_id: command.targetId, request_id: requestId })}`);
|
|
124
|
+
if (door.json)
|
|
125
|
+
return emitAgentDoor(door, { ok: true, ...answer.body });
|
|
126
|
+
writeLine(door.io.stdout, `Reminder ${stored?.id ?? ""} set for ${stored?.due_at ?? command.scheduleAt} (${stored?.timezone ?? when.zone ?? "UTC"}). It shows under \`cockpit msg scheduled --state fired\` when due; nothing is posted.`);
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
async function listVerb(command, door) {
|
|
130
|
+
const query = new URLSearchParams();
|
|
131
|
+
if (command.scheduleState)
|
|
132
|
+
query.set("state", command.scheduleState);
|
|
133
|
+
if (command.before)
|
|
134
|
+
query.set("before", command.before);
|
|
135
|
+
if (command.limit)
|
|
136
|
+
query.set("limit", String(command.limit));
|
|
137
|
+
const answer = await askAgentDoor(door, {
|
|
138
|
+
path: `/api/msg/schedules${query.size > 0 ? `?${query.toString()}` : ""}`,
|
|
139
|
+
method: "GET",
|
|
140
|
+
label: "msg scheduled",
|
|
141
|
+
timeoutMs: DEADLINE_MS,
|
|
142
|
+
});
|
|
143
|
+
if (!answer.ok)
|
|
144
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
145
|
+
const body = answer.body;
|
|
146
|
+
const items = body.items ?? [];
|
|
147
|
+
writeLine(door.io.stderr, `${TAG} scheduled ${JSON.stringify({ state: command.scheduleState ?? "open", returned: items.length, has_more: body.hasMore === true })}`);
|
|
148
|
+
if (door.json)
|
|
149
|
+
return emitAgentDoor(door, { ok: true, ...answer.body });
|
|
150
|
+
for (const row of items)
|
|
151
|
+
writeLine(door.io.stdout, `${describe(row)} ${oneLine(row.body)}`);
|
|
152
|
+
if (items.length === 0)
|
|
153
|
+
writeLine(door.io.stdout, "Nothing scheduled.");
|
|
154
|
+
writeLine(door.io.stdout, body.hasMore && body.nextCursor ? `more: --before ${body.nextCursor}` : "complete");
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
async function changeVerb(command, door) {
|
|
158
|
+
const body = await readBody(command, door, false);
|
|
159
|
+
if (!body.ok)
|
|
160
|
+
return body.code;
|
|
161
|
+
if (!command.scheduleAt && body.text === "") {
|
|
162
|
+
return failAgentDoor(door, TAG, "invalid_body", "Change the time with --at, the text by piping it in, or both.");
|
|
163
|
+
}
|
|
164
|
+
const when = command.scheduleAt ? atField(command) : null;
|
|
165
|
+
if (when)
|
|
166
|
+
sayAssumedZone(door, when.zone, when.assumedZone);
|
|
167
|
+
const answer = await askAgentDoor(door, {
|
|
168
|
+
path: `/api/msg/schedules/${encodeURIComponent(command.targetId ?? "")}`,
|
|
169
|
+
method: "PATCH",
|
|
170
|
+
label: "msg scheduled change",
|
|
171
|
+
timeoutMs: DEADLINE_MS,
|
|
172
|
+
body: {
|
|
173
|
+
...(when ? { at: when.at, ...(when.zone && typeof when.at === "string" ? { timezone: when.zone } : {}) } : {}),
|
|
174
|
+
...(body.text !== "" ? { body: body.text } : {}),
|
|
175
|
+
...(command.scheduleRevision === undefined ? {} : { expected_revision: command.scheduleRevision }),
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
if (!answer.ok)
|
|
179
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail, scheduleExtra(answer.body));
|
|
180
|
+
const result = answer.body;
|
|
181
|
+
writeLine(door.io.stderr, `${TAG} change ${JSON.stringify({ schedule_id: command.targetId, changed: result.changed !== false })}`);
|
|
182
|
+
if (door.json)
|
|
183
|
+
return emitAgentDoor(door, { ok: true, ...answer.body });
|
|
184
|
+
writeLine(door.io.stdout, result.changed === false
|
|
185
|
+
? `Unchanged: ${command.targetId} already has that time and text.`
|
|
186
|
+
: `Changed ${command.targetId}: now due ${result.schedule?.due_at} (${result.schedule?.timezone}), revision ${result.schedule?.revision}.`);
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
function scheduleExtra(body) {
|
|
190
|
+
const mutation = body?.mutation;
|
|
191
|
+
return mutation ? { mutation } : {};
|
|
192
|
+
}
|
|
193
|
+
async function cancelOrDone(command, door) {
|
|
194
|
+
const done = command.scheduleVerb === "done";
|
|
195
|
+
const answer = await askAgentDoor(door, {
|
|
196
|
+
path: `/api/msg/schedules/${encodeURIComponent(command.targetId ?? "")}${done ? "/done" : ""}`,
|
|
197
|
+
method: done ? "POST" : "DELETE",
|
|
198
|
+
label: `msg scheduled ${done ? "done" : "cancel"}`,
|
|
199
|
+
timeoutMs: DEADLINE_MS,
|
|
200
|
+
});
|
|
201
|
+
if (!answer.ok) {
|
|
202
|
+
const sentId = answer.body?.mutation?.message_id;
|
|
203
|
+
if (answer.reason === "schedule_already_sent" && sentId)
|
|
204
|
+
writeLine(door.io.stderr, `${TAG} it already went out as message ${sentId}.`);
|
|
205
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail, scheduleExtra(answer.body));
|
|
206
|
+
}
|
|
207
|
+
const result = answer.body;
|
|
208
|
+
writeLine(door.io.stderr, `${TAG} ${done ? "done" : "cancel"} ${JSON.stringify({ schedule_id: command.targetId, changed: result.changed !== false })}`);
|
|
209
|
+
if (door.json)
|
|
210
|
+
return emitAgentDoor(door, { ok: true, ...answer.body });
|
|
211
|
+
const past = done ? "Marked done" : "Cancelled";
|
|
212
|
+
writeLine(door.io.stdout, result.changed === false ? `${past} already: ${command.targetId}.` : `${past} ${command.targetId}.`);
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
export async function runMsgSchedule(command, door, resolve) {
|
|
216
|
+
if (command.action === "schedule")
|
|
217
|
+
return scheduleVerb(command, door, resolve);
|
|
218
|
+
if (command.action === "remind")
|
|
219
|
+
return remindVerb(command, door);
|
|
220
|
+
switch (command.scheduleVerb ?? "list") {
|
|
221
|
+
case "change": return changeVerb(command, door);
|
|
222
|
+
case "cancel":
|
|
223
|
+
case "done": return cancelOrDone(command, door);
|
|
224
|
+
default: return listVerb(command, door);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The message-search flags shared by `cockpit msg search` and `cockpit search --kind msg` (BLI-21658, slice 8):
|
|
3
|
+
* `--from <email|me>` (repeatable), `--in <channel>`, `--since` / `--until` (`YYYY-MM-DD`, both inclusive local
|
|
4
|
+
* days), `--timezone <IANA>`, `--replies` or `--top-level`, `--has-file`. One reader, one query-string writer, so
|
|
5
|
+
* the two commands cannot mean different things. The door checks the values; this file only checks shape.
|
|
6
|
+
*/
|
|
7
|
+
import { optionalNonEmpty } from "./local-arg-values.js";
|
|
8
|
+
export const MSG_SEARCH_VALUE_FLAGS = ["--in", "--since", "--until"];
|
|
9
|
+
export const MSG_SEARCH_BOOLEAN_FLAGS = ["--replies", "--top-level", "--has-file"];
|
|
10
|
+
/** Flags only a message search reads (`--from` and `--timezone` are shared with other verbs, so they are not listed). */
|
|
11
|
+
export const MSG_SEARCH_ONLY_FLAGS = [...MSG_SEARCH_VALUE_FLAGS, ...MSG_SEARCH_BOOLEAN_FLAGS];
|
|
12
|
+
const DAY = /^\d{4}-\d{2}-\d{2}$/;
|
|
13
|
+
function machineZone() {
|
|
14
|
+
try {
|
|
15
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return "UTC";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function day(flag, value) {
|
|
22
|
+
const given = optionalNonEmpty(value);
|
|
23
|
+
if (given === undefined)
|
|
24
|
+
return undefined;
|
|
25
|
+
if (!DAY.test(given))
|
|
26
|
+
throw new Error(`${flag} takes a day written YYYY-MM-DD, not "${given}".`);
|
|
27
|
+
return given;
|
|
28
|
+
}
|
|
29
|
+
/** Reads the filter flags. The machine's IANA zone is sent unless `--timezone` is given. */
|
|
30
|
+
export function readMsgSearchFilters(values) {
|
|
31
|
+
const replies = values.booleans.has("--replies");
|
|
32
|
+
const topLevel = values.booleans.has("--top-level");
|
|
33
|
+
if (replies && topLevel)
|
|
34
|
+
throw new Error("Pick one of --replies or --top-level.");
|
|
35
|
+
const from = (values.flagValues.get("--from") ?? []).map((entry) => entry.trim()).filter((entry) => entry !== "");
|
|
36
|
+
const channel = optionalNonEmpty(values.flags.get("--in"));
|
|
37
|
+
const since = day("--since", values.flags.get("--since"));
|
|
38
|
+
const until = day("--until", values.flags.get("--until"));
|
|
39
|
+
if (since && until && since > until)
|
|
40
|
+
throw new Error(`--since ${since} is after --until ${until}.`);
|
|
41
|
+
return {
|
|
42
|
+
...(from.length > 0 ? { from } : {}),
|
|
43
|
+
...(channel ? { in: channel } : {}),
|
|
44
|
+
...(since ? { since } : {}),
|
|
45
|
+
...(until ? { until } : {}),
|
|
46
|
+
timezone: optionalNonEmpty(values.flags.get("--timezone")) ?? machineZone(),
|
|
47
|
+
...(replies ? { thread: "reply" } : topLevel ? { thread: "root" } : {}),
|
|
48
|
+
...(values.booleans.has("--has-file") ? { hasFile: true } : {}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** True when a flag that narrows the search was given (the zone alone does not narrow). */
|
|
52
|
+
export function hasMsgSearchFilterFlags(filters) {
|
|
53
|
+
return Boolean(filters.from || filters.in || filters.since || filters.until || filters.thread || filters.hasFile);
|
|
54
|
+
}
|
|
55
|
+
/** The structured query parameters `GET /api/msg/search` and `GET /api/search` read. */
|
|
56
|
+
export function appendMsgSearchParams(params, filters) {
|
|
57
|
+
for (const person of filters.from ?? [])
|
|
58
|
+
params.append("from", person);
|
|
59
|
+
if (filters.in)
|
|
60
|
+
params.set("channel", filters.in);
|
|
61
|
+
if (filters.since)
|
|
62
|
+
params.set("since", filters.since);
|
|
63
|
+
if (filters.until)
|
|
64
|
+
params.set("until", filters.until);
|
|
65
|
+
if (filters.timezone)
|
|
66
|
+
params.set("tz", filters.timezone);
|
|
67
|
+
if (filters.thread)
|
|
68
|
+
params.set("thread", filters.thread);
|
|
69
|
+
if (filters.hasFile)
|
|
70
|
+
params.set("has", "file");
|
|
71
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit msg search [words] [--from <email|me>]... [--in <channel>] [--since YYYY-MM-DD] [--until YYYY-MM-DD]
|
|
3
|
+
* [--timezone <zone>] [--replies|--top-level] [--has-file] [--before <cursor>] [--limit <n>] [--json]` (BLI-21658).
|
|
4
|
+
*
|
|
5
|
+
* The door the browser's message box presses (`GET /api/msg/search`): newest first, paged by `nextCursor`. `--json`
|
|
6
|
+
* prints the door's body unchanged so ids, filters and paging match HTTP. Split out of `msg.ts` for the file-size ratchet.
|
|
7
|
+
*/
|
|
8
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
9
|
+
import { writeLine } from "./cli-io.js";
|
|
10
|
+
import { appendMsgSearchParams } from "./msg-search-args.js";
|
|
11
|
+
const TAG = "[msg cli]";
|
|
12
|
+
const READ_DEADLINE_MS = 30_000;
|
|
13
|
+
const SNIPPET_LENGTH = 140;
|
|
14
|
+
function snippet(content) {
|
|
15
|
+
const flat = (content ?? "").replace(/\s+/g, " ").trim();
|
|
16
|
+
if (flat === "")
|
|
17
|
+
return "(attachment, no text)";
|
|
18
|
+
return flat.length > SNIPPET_LENGTH ? `${flat.slice(0, SNIPPET_LENGTH - 3)}...` : flat;
|
|
19
|
+
}
|
|
20
|
+
export async function runMsgSearch(command, door) {
|
|
21
|
+
const params = new URLSearchParams({ q: command.searchQuery ?? "" });
|
|
22
|
+
appendMsgSearchParams(params, command.searchFilters ?? {});
|
|
23
|
+
if (command.before)
|
|
24
|
+
params.set("before", command.before);
|
|
25
|
+
if (command.limit)
|
|
26
|
+
params.set("limit", String(command.limit));
|
|
27
|
+
const answer = await askAgentDoor(door, {
|
|
28
|
+
path: `/api/msg/search?${params.toString()}`,
|
|
29
|
+
method: "GET",
|
|
30
|
+
label: "msg search",
|
|
31
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
32
|
+
});
|
|
33
|
+
if (!answer.ok)
|
|
34
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
35
|
+
const body = answer.body;
|
|
36
|
+
const hits = body.hits ?? [];
|
|
37
|
+
writeLine(door.io.stderr, `${TAG} search ${JSON.stringify({ returned: hits.length, has_more: body.hasMore === true, coverage: body.coverage ?? null })}`);
|
|
38
|
+
if (door.json)
|
|
39
|
+
return emitAgentDoor(door, { ok: true, ...body });
|
|
40
|
+
if (hits.length === 0)
|
|
41
|
+
writeLine(door.io.stdout, "No messages match.");
|
|
42
|
+
for (const hit of hits) {
|
|
43
|
+
const where = hit.channel ? (hit.channel.is_dm ? "direct message" : `#${hit.channel.name ?? "unnamed"}`) : hit.channel_id;
|
|
44
|
+
writeLine(door.io.stdout, `${hit.id} ${where} ${hit.author_name ?? hit.user_id} ${hit.created_at} ${snippet(hit.content)}`);
|
|
45
|
+
}
|
|
46
|
+
writeLine(door.io.stdout, body.hasMore && body.nextCursor ? `more: --before ${body.nextCursor}` : "complete");
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
@@ -13,7 +13,21 @@ const CONTENT_MAX_CHARS = 8_000;
|
|
|
13
13
|
/** The one sentence a deleted message reads as, on every surface. */
|
|
14
14
|
export const DELETED_SENTENCE = "This message was deleted";
|
|
15
15
|
export function messageText(message) {
|
|
16
|
-
|
|
16
|
+
if (message.deleted_at)
|
|
17
|
+
return `(${DELETED_SENTENCE})`;
|
|
18
|
+
const names = message.files?.map((file) => file.name) ?? [];
|
|
19
|
+
if (message.content)
|
|
20
|
+
return names.length > 0 ? `${message.content} [files: ${names.join(", ")}]` : message.content;
|
|
21
|
+
// BLI-21521: an attachment-only post has NULL content and is not deleted.
|
|
22
|
+
const blockCount = Array.isArray(message.blocks)
|
|
23
|
+
? Number(message.blocks.find((block) => block?.type === "files")?.count ?? 0)
|
|
24
|
+
: 0;
|
|
25
|
+
const count = names.length || blockCount;
|
|
26
|
+
if (count === 1 && names[0])
|
|
27
|
+
return `Shared a file: ${names[0]}`;
|
|
28
|
+
if (count === 1)
|
|
29
|
+
return "Shared a file";
|
|
30
|
+
return count > 1 ? `Shared ${count} files` : "";
|
|
17
31
|
}
|
|
18
32
|
export async function readContent(io) {
|
|
19
33
|
if (isInteractiveStdin(io)) {
|
package/dist/commands/msg.js
CHANGED
|
@@ -17,20 +17,24 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { randomUUID } from "node:crypto";
|
|
19
19
|
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
|
|
20
|
-
import { writeLine } from "./cli-io.js";
|
|
20
|
+
import { isInteractiveStdin, writeLine } from "./cli-io.js";
|
|
21
21
|
import { cacheAgeLabel, cacheCoverage, readThroughCache } from "./read-cache.js";
|
|
22
22
|
import { markReadThrough } from "./msg-read-line.js";
|
|
23
|
+
import { runMsgActivityVerb } from "./msg-activity.js";
|
|
24
|
+
import { runMsgDraft } from "./msg-draft.js";
|
|
25
|
+
import { runMsgSearch } from "./msg-search.js";
|
|
26
|
+
import { openDm } from "./msg-dm.js";
|
|
27
|
+
import { runMsgMark } from "./msg-mark.js";
|
|
28
|
+
import { runMsgChannel } from "./msg-channel.js";
|
|
29
|
+
import { runMsgFile } from "./msg-file.js";
|
|
23
30
|
import { deleteMessage, editMessage, messageOutcome, messageText, readContent, refuseMsgWrite } from "./msg-write.js";
|
|
24
31
|
const TAG = "[msg cli]";
|
|
25
|
-
const READ_DEADLINE_MS = 30_000;
|
|
26
|
-
const WRITE_DEADLINE_MS = 30_000;
|
|
32
|
+
const [READ_DEADLINE_MS, WRITE_DEADLINE_MS] = [30_000, 30_000];
|
|
27
33
|
export async function runMsg(command, io) {
|
|
28
34
|
const door = await openAgentDoor("msg", command, io);
|
|
29
35
|
switch (command.action) {
|
|
30
|
-
case "show":
|
|
31
|
-
|
|
32
|
-
case "channels":
|
|
33
|
-
return listChannels(door);
|
|
36
|
+
case "show": return showMessage(command, door);
|
|
37
|
+
case "channels": return listChannels(door);
|
|
34
38
|
case "read":
|
|
35
39
|
return readChannel(command, door);
|
|
36
40
|
case "unread":
|
|
@@ -43,14 +47,63 @@ export async function runMsg(command, io) {
|
|
|
43
47
|
return deleteMessage(command, door);
|
|
44
48
|
case "outcome":
|
|
45
49
|
return messageOutcome(command, door);
|
|
50
|
+
case "draft":
|
|
51
|
+
return draftVerb(command, door);
|
|
52
|
+
case "file":
|
|
53
|
+
return fileVerb(command, door);
|
|
46
54
|
case "thread":
|
|
47
55
|
return readThread(command, door);
|
|
48
|
-
case "create":
|
|
49
|
-
|
|
50
|
-
case "
|
|
51
|
-
return
|
|
56
|
+
case "create": return createChannel(command, door);
|
|
57
|
+
case "dm": return openDm(command, door);
|
|
58
|
+
case "search":
|
|
59
|
+
return runMsgSearch(command, door);
|
|
60
|
+
case "channel":
|
|
61
|
+
case "members":
|
|
62
|
+
case "hide":
|
|
63
|
+
case "unhide":
|
|
64
|
+
return runMsgChannel(command, door, (ref) => resolveChannelId(door, ref));
|
|
65
|
+
case "save":
|
|
66
|
+
case "saved":
|
|
67
|
+
case "pin":
|
|
68
|
+
case "pins":
|
|
69
|
+
case "forward":
|
|
70
|
+
case "schedule":
|
|
71
|
+
case "remind":
|
|
72
|
+
case "scheduled":
|
|
73
|
+
case "context":
|
|
74
|
+
return runMsgMark(command, door, (ref) => resolveChannelId(door, ref));
|
|
75
|
+
case "activity":
|
|
76
|
+
case "follow":
|
|
77
|
+
case "unfollow":
|
|
78
|
+
case "react":
|
|
79
|
+
case "notify":
|
|
80
|
+
return runMsgActivityVerb(command, door, (ref) => resolveChannelId(door, ref));
|
|
52
81
|
}
|
|
53
82
|
}
|
|
83
|
+
/** `msg draft`: `list` needs no channel; every other verb resolves the channel the same way read and send do. */
|
|
84
|
+
async function draftVerb(command, door) {
|
|
85
|
+
if ((command.draftVerb ?? "list") === "list")
|
|
86
|
+
return runMsgDraft(command, door, null);
|
|
87
|
+
const ref = command.channelRef ?? "";
|
|
88
|
+
const resolved = await resolveChannelId(door, ref);
|
|
89
|
+
if (resolved.status === "list_failed")
|
|
90
|
+
return failAgentDoor(door, TAG, resolved.reason, resolved.detail);
|
|
91
|
+
if (resolved.status === "not_found")
|
|
92
|
+
return channelNotFound(door, ref);
|
|
93
|
+
return runMsgDraft(command, door, resolved.id);
|
|
94
|
+
}
|
|
95
|
+
/** `msg file`: `upload` and `list` name a channel; the other verbs name a file. */
|
|
96
|
+
async function fileVerb(command, door) {
|
|
97
|
+
if (command.fileVerb !== "upload" && command.fileVerb !== "list")
|
|
98
|
+
return runMsgFile(command, door, null);
|
|
99
|
+
const ref = command.channelRef ?? "";
|
|
100
|
+
const resolved = await resolveChannelId(door, ref);
|
|
101
|
+
if (resolved.status === "list_failed")
|
|
102
|
+
return failAgentDoor(door, TAG, resolved.reason, resolved.detail);
|
|
103
|
+
if (resolved.status === "not_found")
|
|
104
|
+
return channelNotFound(door, ref);
|
|
105
|
+
return runMsgFile(command, door, resolved.id);
|
|
106
|
+
}
|
|
54
107
|
async function fetchChannels(door) {
|
|
55
108
|
const answer = await readThroughCache({
|
|
56
109
|
io: door.io,
|
|
@@ -261,10 +314,16 @@ async function sendMessage(command, door) {
|
|
|
261
314
|
return failAgentDoor(door, TAG, resolved.reason, resolved.detail);
|
|
262
315
|
if (resolved.status === "not_found")
|
|
263
316
|
return channelNotFound(door, ref);
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (
|
|
317
|
+
// BLI-21521: with --file the text is optional; stdin is read only when something is piped in.
|
|
318
|
+
const fileIds = command.fileIds ?? [];
|
|
319
|
+
let text = "";
|
|
320
|
+
if (fileIds.length === 0 || !isInteractiveStdin(door.io)) {
|
|
321
|
+
const content = await readContent(door.io);
|
|
322
|
+
if (!content.ok)
|
|
323
|
+
return failAgentDoor(door, TAG, content.reason, content.detail);
|
|
324
|
+
text = content.text;
|
|
325
|
+
}
|
|
326
|
+
if (text === "" && fileIds.length === 0)
|
|
268
327
|
return failAgentDoor(door, TAG, "invalid_body", "There was nothing to send. Nothing was sent.");
|
|
269
328
|
// BLI-21314: every send carries a request id, so a lost response can be checked and a retry posts once.
|
|
270
329
|
const requestId = command.requestId ?? randomUUID();
|
|
@@ -274,7 +333,8 @@ async function sendMessage(command, door) {
|
|
|
274
333
|
label: "msg send",
|
|
275
334
|
timeoutMs: WRITE_DEADLINE_MS,
|
|
276
335
|
body: {
|
|
277
|
-
content:
|
|
336
|
+
...(text === "" ? {} : { content: text }),
|
|
337
|
+
...(fileIds.length > 0 ? { file_ids: fileIds } : {}),
|
|
278
338
|
request_id: requestId,
|
|
279
339
|
...(command.threadId ? { thread_parent_id: command.threadId } : {}),
|
|
280
340
|
},
|
|
@@ -282,7 +342,7 @@ async function sendMessage(command, door) {
|
|
|
282
342
|
if (!answer.ok)
|
|
283
343
|
return refuseMsgWrite(door, answer, requestId, "send");
|
|
284
344
|
const { message = null, mutation = null } = answer.body;
|
|
285
|
-
writeLine(door.io.stderr, `${TAG} sent ${JSON.stringify({ channel_id: resolved.id, message_id: message?.id ?? null, request_id: requestId, chars:
|
|
345
|
+
writeLine(door.io.stderr, `${TAG} sent ${JSON.stringify({ channel_id: resolved.id, message_id: message?.id ?? null, request_id: requestId, chars: text.length, files: fileIds.length })}`);
|
|
286
346
|
if (door.json)
|
|
287
347
|
return emitAgentDoor(door, { ok: true, message, mutation });
|
|
288
348
|
writeLine(door.io.stdout, `Sent to ${ref} (${message?.id ?? "?"}).`);
|
|
@@ -345,28 +405,6 @@ function reportChannelCreationToHuman(door, channel, name, requestedMembers, mem
|
|
|
345
405
|
writeLine(door.io.stdout, `Not added (${failure.reason}): ${failure.userId}`);
|
|
346
406
|
}
|
|
347
407
|
}
|
|
348
|
-
async function openDm(command, door) {
|
|
349
|
-
const email = command.dmEmail ?? "";
|
|
350
|
-
if (email === "")
|
|
351
|
-
return failAgentDoor(door, TAG, "invalid_body", "msg dm needs an email address.");
|
|
352
|
-
const answer = await askAgentDoor(door, {
|
|
353
|
-
path: "/api/msg/channels",
|
|
354
|
-
method: "POST",
|
|
355
|
-
label: "msg dm",
|
|
356
|
-
timeoutMs: WRITE_DEADLINE_MS,
|
|
357
|
-
body: { dm_participant_emails: [email] },
|
|
358
|
-
});
|
|
359
|
-
if (!answer.ok)
|
|
360
|
-
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
361
|
-
const channel = answer.body.channel ?? null;
|
|
362
|
-
writeLine(door.io.stderr, `${TAG} dm resolved ${JSON.stringify({ channel_id: channel?.id ?? null })}`);
|
|
363
|
-
if (door.json)
|
|
364
|
-
return emitAgentDoor(door, { ok: true, channel });
|
|
365
|
-
// Idempotent by contract (`get_or_create_dm`), so the wording says what is
|
|
366
|
-
// true either way rather than claiming a creation that may not have happened.
|
|
367
|
-
writeLine(door.io.stdout, `Direct message with ${email}: ${channel?.id ?? "?"}`);
|
|
368
|
-
return 0;
|
|
369
|
-
}
|
|
370
408
|
async function showMessage(command, door) {
|
|
371
409
|
const answer = await askAgentDoor(door, { path: `/api/msg/messages/${encodeURIComponent(command.threadId ?? "")}`, method: "GET", label: "msg show", timeoutMs: READ_DEADLINE_MS });
|
|
372
410
|
if (!answer.ok)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit project` argument parsing (BLI-3716 list, BLI-21659 show/create/update/archive/restore).
|
|
3
|
+
*
|
|
4
|
+
* A description never travels on argv by default: `--file <path>` reads it from a local file (safest on Windows);
|
|
5
|
+
* `--description <text>` is the short form and `--description none` clears it. Write-safety flags
|
|
6
|
+
* (`--request-id`, `--expected-updated-at`) belong to the four write verbs.
|
|
7
|
+
*/
|
|
8
|
+
import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
9
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
10
|
+
const ACTIONS = ["list", "show", "create", "update", "archive", "restore"];
|
|
11
|
+
const WRITES = ["create", "update", "archive", "restore"];
|
|
12
|
+
export function parseProjectArgs(args) {
|
|
13
|
+
const values = parseNamedArgs(args, {
|
|
14
|
+
allowedFlags: [
|
|
15
|
+
"--home", "--dashboard-url", "--archived", "--cursor", "--limit", "--json",
|
|
16
|
+
"--name", "--description", "--file", "--color", "--request-id", "--expected-updated-at",
|
|
17
|
+
],
|
|
18
|
+
valueFlags: [
|
|
19
|
+
"--home", "--dashboard-url", "--cursor", "--limit",
|
|
20
|
+
"--name", "--description", "--file", "--color", "--request-id", "--expected-updated-at",
|
|
21
|
+
],
|
|
22
|
+
});
|
|
23
|
+
const first = values.positionals[0];
|
|
24
|
+
const action = (first === undefined ? "list" : first);
|
|
25
|
+
if (!ACTIONS.includes(action)) {
|
|
26
|
+
throw new Error(`Unknown project command: ${first}. Try list, show, create, update, archive or restore.`);
|
|
27
|
+
}
|
|
28
|
+
const rest = values.positionals.slice(first === undefined ? 0 : 1);
|
|
29
|
+
const needsRef = action !== "list" && action !== "create";
|
|
30
|
+
if (needsRef && rest.length !== 1)
|
|
31
|
+
throw new Error(`project ${action} takes exactly one project id or name.`);
|
|
32
|
+
if (!needsRef && rest.length > 0)
|
|
33
|
+
throw new Error(`project ${action} does not take "${rest[0]}".`);
|
|
34
|
+
const given = (flag) => values.flags.has(flag);
|
|
35
|
+
const isWrite = WRITES.includes(action);
|
|
36
|
+
if (!isWrite && (given("--request-id") || given("--expected-updated-at"))) {
|
|
37
|
+
throw new Error("--request-id and --expected-updated-at belong to create, update, archive and restore.");
|
|
38
|
+
}
|
|
39
|
+
if (action !== "create" && action !== "update" && (given("--name") || given("--description") || given("--file"))) {
|
|
40
|
+
throw new Error("--name, --description and --file belong to project create and update.");
|
|
41
|
+
}
|
|
42
|
+
if (action !== "create" && given("--color"))
|
|
43
|
+
throw new Error("--color belongs to project create.");
|
|
44
|
+
if (action !== "list" && (given("--cursor") || given("--limit") || values.booleans.has("--archived"))) {
|
|
45
|
+
throw new Error("--archived, --cursor and --limit belong to project list.");
|
|
46
|
+
}
|
|
47
|
+
if (given("--description") && given("--file"))
|
|
48
|
+
throw new Error("Give --description or --file, not both.");
|
|
49
|
+
const requestId = optionalNonEmpty(values.flags.get("--request-id"));
|
|
50
|
+
if (requestId && !UUID_PATTERN.test(requestId))
|
|
51
|
+
throw new Error("--request-id must be a UUID.");
|
|
52
|
+
const expectedUpdatedAt = optionalNonEmpty(values.flags.get("--expected-updated-at"));
|
|
53
|
+
if (expectedUpdatedAt && Number.isNaN(Date.parse(expectedUpdatedAt))) {
|
|
54
|
+
throw new Error("--expected-updated-at must be the updated_at timestamp you last read.");
|
|
55
|
+
}
|
|
56
|
+
const name = optionalNonEmpty(values.flags.get("--name"));
|
|
57
|
+
if (action === "create" && !name)
|
|
58
|
+
throw new Error('project create needs --name "<name>".');
|
|
59
|
+
const rawDescription = values.flags.get("--description");
|
|
60
|
+
const description = rawDescription === undefined ? undefined : rawDescription.trim().toLowerCase() === "none" ? null : rawDescription;
|
|
61
|
+
const filePath = optionalNonEmpty(values.flags.get("--file"));
|
|
62
|
+
if (action === "update" && name === undefined && description === undefined && filePath === undefined) {
|
|
63
|
+
throw new Error("project update needs --name, --description (or none) or --file.");
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
kind: "project",
|
|
67
|
+
action,
|
|
68
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
69
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
70
|
+
includeArchived: values.booleans.has("--archived"),
|
|
71
|
+
...(optionalNonEmpty(values.flags.get("--cursor")) ? { cursor: optionalNonEmpty(values.flags.get("--cursor")) } : {}),
|
|
72
|
+
...(values.flags.has("--limit") ? { limit: optionalPositiveInteger(values.flags.get("--limit"), "--limit") } : {}),
|
|
73
|
+
...(needsRef ? { ref: rest[0] } : {}),
|
|
74
|
+
...(name === undefined ? {} : { name }),
|
|
75
|
+
...(description === undefined ? {} : { description }),
|
|
76
|
+
...(filePath === undefined ? {} : { filePath }),
|
|
77
|
+
...(optionalNonEmpty(values.flags.get("--color")) ? { color: optionalNonEmpty(values.flags.get("--color")) } : {}),
|
|
78
|
+
...(requestId ? { requestId } : {}),
|
|
79
|
+
...(expectedUpdatedAt ? { expectedUpdatedAt } : {}),
|
|
80
|
+
json: values.booleans.has("--json"),
|
|
81
|
+
};
|
|
82
|
+
}
|