@bli-cockpit/cli 0.2.239 → 0.2.240
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/cal-delete.js +13 -3
- package/dist/commands/cal-scope.js +71 -0
- package/dist/commands/cal.js +7 -0
- package/dist/commands/docs-share.js +81 -0
- package/dist/commands/docs.js +5 -0
- package/dist/commands/drills-render.js +2 -2
- package/dist/commands/drills.js +4 -4
- package/dist/commands/issue-list-page.js +101 -0
- package/dist/commands/issue.js +23 -5
- package/dist/commands/local-args-tower-cal.js +97 -6
- package/dist/commands/local-args-tower-docs-msg.js +30 -3
- package/dist/commands/local-args-tower-drills.js +1 -1
- package/dist/commands/local-args-tower-msg.js +43 -6
- package/dist/commands/local-args-tower-work.js +74 -6
- package/dist/commands/local-help-commands-tower-ops.js +3 -3
- package/dist/commands/local-help-commands-tower.js +14 -10
- package/dist/commands/msg-write.js +118 -0
- package/dist/commands/msg.js +20 -29
- package/dist/commands/project.js +15 -3
- package/dist/commands/public-root.js +1 -1
- package/package.json +2 -2
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs, } from "./local-arg-values.js";
|
|
10
10
|
import { parseDocsExportFlags } from "./local-args-docs-export.js";
|
|
11
|
-
const DOCS_ACTIONS = new Set(["list", "tree", "read", "create", "update", "delete", "link", "sync-status", "export", "mutation-status"]);
|
|
11
|
+
const DOCS_ACTIONS = new Set(["list", "tree", "read", "create", "update", "delete", "link", "sync-status", "export", "mutation-status", "share", "access"]);
|
|
12
12
|
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
13
13
|
// BLI-10450: `read` moved to its own branch in `parseDocsDocumentReference` —
|
|
14
14
|
// it takes SEVERAL references now, so it no longer shares the "exactly one"
|
|
15
15
|
// rule `update`/`link` still enforce. BLI‑10555: `delete` takes several too
|
|
16
16
|
// (the same door, called once per ref), so it joined `read`'s branch rather
|
|
17
17
|
// than this set.
|
|
18
|
-
const DOCS_ACTIONS_NEEDING_A_DOC = new Set(["update", "link", "export", "mutation-status"]);
|
|
18
|
+
const DOCS_ACTIONS_NEEDING_A_DOC = new Set(["update", "link", "export", "mutation-status", "share", "access"]);
|
|
19
19
|
const DOCS_ALLOWED_FLAGS = [
|
|
20
20
|
"--home",
|
|
21
21
|
"--dashboard-url",
|
|
@@ -37,6 +37,9 @@ const DOCS_ALLOWED_FLAGS = [
|
|
|
37
37
|
"--offset",
|
|
38
38
|
"--format",
|
|
39
39
|
"--out",
|
|
40
|
+
"--add",
|
|
41
|
+
"--remove",
|
|
42
|
+
"--reason",
|
|
40
43
|
"--json",
|
|
41
44
|
];
|
|
42
45
|
const DOCS_VALUE_FLAGS = [
|
|
@@ -54,6 +57,9 @@ const DOCS_VALUE_FLAGS = [
|
|
|
54
57
|
"--offset",
|
|
55
58
|
"--format",
|
|
56
59
|
"--out",
|
|
60
|
+
"--add",
|
|
61
|
+
"--remove",
|
|
62
|
+
"--reason",
|
|
57
63
|
];
|
|
58
64
|
export function parseDocsArgs(args) {
|
|
59
65
|
const values = parseNamedArgs(args, { allowedFlags: DOCS_ALLOWED_FLAGS, valueFlags: DOCS_VALUE_FLAGS });
|
|
@@ -123,7 +129,7 @@ function parseDocsAction(positionals) {
|
|
|
123
129
|
const first = positionals[0];
|
|
124
130
|
const action = (first === undefined ? "list" : first);
|
|
125
131
|
if (!DOCS_ACTIONS.has(action)) {
|
|
126
|
-
throw new Error(`Unknown docs command: ${first}. Try list, tree, read, create, update, delete, link, sync-status, export,
|
|
132
|
+
throw new Error(`Unknown docs command: ${first}. Try list, tree, read, create, update, delete, link, sync-status, export, mutation-status, share, or access.`);
|
|
127
133
|
}
|
|
128
134
|
return { action, rest: positionals.slice(first === undefined ? 0 : 1) };
|
|
129
135
|
}
|
|
@@ -233,6 +239,26 @@ function parseDocsReadFlags(action, values) {
|
|
|
233
239
|
throw new Error("--offset belongs to `cockpit docs read`.");
|
|
234
240
|
return { query, wholeWord, limit, bodies, section, offset };
|
|
235
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* BLI-21313: `docs share <doc> --add <email> | --remove <email> [--reason <text>]` names or removes a page editor;
|
|
244
|
+
* `docs access <doc>` reads who may edit and why. A person is an exact email address, never a fuzzy name.
|
|
245
|
+
*/
|
|
246
|
+
function parseDocsShareFlags(action, values) {
|
|
247
|
+
const addPerson = optionalNonEmpty(values.flags.get("--add"));
|
|
248
|
+
const removePerson = optionalNonEmpty(values.flags.get("--remove"));
|
|
249
|
+
const reason = optionalNonEmpty(values.flags.get("--reason"));
|
|
250
|
+
if ((addPerson || removePerson || reason) && action !== "share") {
|
|
251
|
+
throw new Error("--add, --remove and --reason belong to `cockpit docs share`.");
|
|
252
|
+
}
|
|
253
|
+
if (action === "share") {
|
|
254
|
+
if (Boolean(addPerson) === Boolean(removePerson)) {
|
|
255
|
+
throw new Error("docs share needs exactly one of --add <email> or --remove <email>.");
|
|
256
|
+
}
|
|
257
|
+
if (reason && reason.length > 200)
|
|
258
|
+
throw new Error("--reason can be at most 200 characters.");
|
|
259
|
+
}
|
|
260
|
+
return { ...(addPerson ? { addPerson } : {}), ...(removePerson ? { removePerson } : {}), ...(reason ? { reason } : {}) };
|
|
261
|
+
}
|
|
236
262
|
function parseDocsOffset(raw) {
|
|
237
263
|
const offsetRaw = optionalNonEmpty(raw);
|
|
238
264
|
if (offsetRaw === undefined)
|
|
@@ -268,6 +294,7 @@ function buildDocsCommand(values, action, docRef, docRefs, visibility, clearPare
|
|
|
268
294
|
...(link.notionPage ? { notionPage: link.notionPage } : {}),
|
|
269
295
|
...(link.clearLink ? { clearLink: true } : {}),
|
|
270
296
|
...parseDocsExportFlags(action, values),
|
|
297
|
+
...parseDocsShareFlags(action, values),
|
|
271
298
|
json: values.booleans.has("--json"),
|
|
272
299
|
};
|
|
273
300
|
}
|
|
@@ -54,7 +54,7 @@ export function parseDrillsArgs(args) {
|
|
|
54
54
|
if (action === "theme-set") {
|
|
55
55
|
theme = rest.join(" ").trim();
|
|
56
56
|
if (!theme)
|
|
57
|
-
throw new Error("`cockpit drills theme set` takes a theme: video-game,
|
|
57
|
+
throw new Error("`cockpit drills theme set` takes a theme: plain (no theme, the default), video-game, sports, movies-tv, music, anime, cooking, or anything you're into in your own words.");
|
|
58
58
|
}
|
|
59
59
|
else if (rest.length > 0)
|
|
60
60
|
throw new Error(`\`cockpit drills ${action}\` takes no positional arguments.`);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
2
|
-
const
|
|
2
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3
|
+
const MSG_ACTIONS = new Set(["channels", "read", "send", "thread", "create", "dm", "show", "unread", "edit", "delete", "outcome"]);
|
|
3
4
|
const MSG_ACTIONS_NEEDING_A_CHANNEL = new Set(["read", "send", "unread"]);
|
|
4
5
|
/**
|
|
5
6
|
* `--members a@x.test,b@y.test` — addresses only, split on commas, never
|
|
@@ -37,9 +38,14 @@ export function parseMsgArgs(args) {
|
|
|
37
38
|
"--description",
|
|
38
39
|
"--from",
|
|
39
40
|
"--mark-read",
|
|
41
|
+
"--request-id",
|
|
42
|
+
"--expected-revision",
|
|
40
43
|
"--json",
|
|
41
44
|
],
|
|
42
|
-
valueFlags: [
|
|
45
|
+
valueFlags: [
|
|
46
|
+
"--home", "--dashboard-url", "--channel", "--thread", "--limit", "--members", "--description", "--before", "--from",
|
|
47
|
+
"--request-id", "--expected-revision",
|
|
48
|
+
],
|
|
43
49
|
});
|
|
44
50
|
const { action, rest } = parseMsgAction(values.positionals);
|
|
45
51
|
if (values.flags.has("--before") && action !== "read" && action !== "thread") {
|
|
@@ -49,7 +55,35 @@ export function parseMsgArgs(args) {
|
|
|
49
55
|
const { channelName, dmEmail } = parseMsgCreateTarget(action, rest);
|
|
50
56
|
const { isPrivate, memberEmails, description } = parseMsgCreateFlags(action, values);
|
|
51
57
|
const { channelRef, threadId, limit } = parseMsgReadTarget(action, rest, values);
|
|
52
|
-
|
|
58
|
+
const writeSafety = parseMsgWriteSafety(action, rest, values);
|
|
59
|
+
return buildMsgCommand(values, action, channelRef, channelName, dmEmail, memberEmails, description, isPrivate, threadId, limit, markRead, unreadFrom, writeSafety);
|
|
60
|
+
}
|
|
61
|
+
/** `send|edit|delete --request-id`, `edit|delete --expected-revision`, and the id `edit`/`delete`/`outcome` name (BLI-21314). */
|
|
62
|
+
function parseMsgWriteSafety(action, rest, values) {
|
|
63
|
+
const requestId = optionalNonEmpty(values.flags.get("--request-id"));
|
|
64
|
+
if (requestId && !UUID_PATTERN.test(requestId))
|
|
65
|
+
throw new Error("--request-id must be a UUID.");
|
|
66
|
+
if (requestId && action !== "send" && action !== "edit" && action !== "delete") {
|
|
67
|
+
throw new Error("--request-id belongs to `cockpit msg send`, `edit` or `delete`.");
|
|
68
|
+
}
|
|
69
|
+
const expectedRevision = optionalPositiveInteger(values.flags.get("--expected-revision"), "--expected-revision");
|
|
70
|
+
if (expectedRevision !== undefined && action !== "edit" && action !== "delete") {
|
|
71
|
+
throw new Error("--expected-revision belongs to `cockpit msg edit` or `delete`.");
|
|
72
|
+
}
|
|
73
|
+
let targetId;
|
|
74
|
+
if (action === "edit" || action === "delete" || action === "outcome") {
|
|
75
|
+
targetId = optionalNonEmpty(rest[0]);
|
|
76
|
+
if (!targetId) {
|
|
77
|
+
throw new Error(action === "outcome"
|
|
78
|
+
? "msg outcome needs the request UUID, e.g. `cockpit msg outcome <request-uuid>`."
|
|
79
|
+
: `msg ${action} needs a message id, e.g. \`cockpit msg ${action} <message-id>\`.`);
|
|
80
|
+
}
|
|
81
|
+
if (rest.length > 1)
|
|
82
|
+
throw new Error(`msg ${action} takes one id, not ${rest.length}.`);
|
|
83
|
+
if (action === "outcome" && !UUID_PATTERN.test(targetId))
|
|
84
|
+
throw new Error("msg outcome takes a request UUID.");
|
|
85
|
+
}
|
|
86
|
+
return { targetId, requestId, expectedRevision };
|
|
53
87
|
}
|
|
54
88
|
/** `read --mark-read` and `unread --from <message-id>` (BLI-21299): the read line, moved on purpose. */
|
|
55
89
|
function parseMsgReadLineFlags(action, values) {
|
|
@@ -68,7 +102,7 @@ function parseMsgAction(positionals) {
|
|
|
68
102
|
const first = positionals[0];
|
|
69
103
|
const action = (first === undefined ? "channels" : first);
|
|
70
104
|
if (!MSG_ACTIONS.has(action)) {
|
|
71
|
-
throw new Error(`Unknown msg command: ${first}. Try channels, read, unread, send, thread, create, dm, or show.`);
|
|
105
|
+
throw new Error(`Unknown msg command: ${first}. Try channels, read, unread, send, thread, create, dm, or show; to change a message: edit, delete or outcome.`);
|
|
72
106
|
}
|
|
73
107
|
return { action, rest: positionals.slice(first === undefined ? 0 : 1) };
|
|
74
108
|
}
|
|
@@ -129,7 +163,7 @@ function parseMsgReadTarget(action, rest, values) {
|
|
|
129
163
|
if (rest.length > 1)
|
|
130
164
|
throw new Error(`msg thread takes one thread id, not ${rest.length}.`);
|
|
131
165
|
}
|
|
132
|
-
else if (action !== "create" && action !== "dm" && rest.length > 0) {
|
|
166
|
+
else if (action !== "create" && action !== "dm" && action !== "edit" && action !== "delete" && action !== "outcome" && rest.length > 0) {
|
|
133
167
|
throw new Error(`msg ${action} does not take "${rest[0]}".`);
|
|
134
168
|
}
|
|
135
169
|
const threadFlag = optionalNonEmpty(values.flags.get("--thread"));
|
|
@@ -145,7 +179,7 @@ function parseMsgReadTarget(action, rest, values) {
|
|
|
145
179
|
}
|
|
146
180
|
return { channelRef, threadId, limit };
|
|
147
181
|
}
|
|
148
|
-
function buildMsgCommand(values, action, channelRef, channelName, dmEmail, memberEmails, description, isPrivate, threadId, limit, markRead, unreadFrom) {
|
|
182
|
+
function buildMsgCommand(values, action, channelRef, channelName, dmEmail, memberEmails, description, isPrivate, threadId, limit, markRead, unreadFrom, writeSafety) {
|
|
149
183
|
return {
|
|
150
184
|
kind: "msg",
|
|
151
185
|
action,
|
|
@@ -162,6 +196,9 @@ function buildMsgCommand(values, action, channelRef, channelName, dmEmail, membe
|
|
|
162
196
|
...(limit === undefined ? {} : { limit }),
|
|
163
197
|
...(markRead ? { markRead } : {}),
|
|
164
198
|
...(unreadFrom ? { unreadFrom } : {}),
|
|
199
|
+
...(writeSafety.targetId ? { targetId: writeSafety.targetId } : {}),
|
|
200
|
+
...(writeSafety.requestId ? { requestId: writeSafety.requestId } : {}),
|
|
201
|
+
...(writeSafety.expectedRevision === undefined ? {} : { expectedRevision: writeSafety.expectedRevision }),
|
|
165
202
|
json: values.booleans.has("--json"),
|
|
166
203
|
};
|
|
167
204
|
}
|
|
@@ -68,6 +68,15 @@ const ISSUE_ALLOWED_FLAGS = [
|
|
|
68
68
|
"--parent",
|
|
69
69
|
"--file",
|
|
70
70
|
"--version",
|
|
71
|
+
"--cursor",
|
|
72
|
+
"--q",
|
|
73
|
+
"--created-after",
|
|
74
|
+
"--created-before",
|
|
75
|
+
"--updated-after",
|
|
76
|
+
"--updated-before",
|
|
77
|
+
"--sort",
|
|
78
|
+
"--dir",
|
|
79
|
+
"--all",
|
|
71
80
|
"--body-stdin",
|
|
72
81
|
"--json",
|
|
73
82
|
];
|
|
@@ -88,6 +97,14 @@ const ISSUE_VALUE_FLAGS = [
|
|
|
88
97
|
"--parent",
|
|
89
98
|
"--file",
|
|
90
99
|
"--version",
|
|
100
|
+
"--cursor",
|
|
101
|
+
"--q",
|
|
102
|
+
"--created-after",
|
|
103
|
+
"--created-before",
|
|
104
|
+
"--updated-after",
|
|
105
|
+
"--updated-before",
|
|
106
|
+
"--sort",
|
|
107
|
+
"--dir",
|
|
91
108
|
];
|
|
92
109
|
export function parseIssueArgs(args) {
|
|
93
110
|
const values = parseNamedArgs(args, {
|
|
@@ -108,9 +125,13 @@ export function parseIssueArgs(args) {
|
|
|
108
125
|
validateIssueState(moveState, "issue move state must be one of: ");
|
|
109
126
|
const stateFilter = readIssueStateFilter(action, values);
|
|
110
127
|
const title = readIssueTitle(action, values);
|
|
111
|
-
if (action !== "board" &&
|
|
112
|
-
throw new Error("--
|
|
128
|
+
if (action !== "board" && values.flags.has("--rows")) {
|
|
129
|
+
throw new Error("--rows widens cockpit tickets board only.");
|
|
113
130
|
}
|
|
131
|
+
if (action !== "board" && action !== "list" && values.flags.has("--label")) {
|
|
132
|
+
throw new Error("--label filters cockpit tickets board and cockpit tickets list.");
|
|
133
|
+
}
|
|
134
|
+
const page = readIssuePage(action, values);
|
|
114
135
|
const boardProject = optionalNonEmpty(values.flags.get("--project"));
|
|
115
136
|
if (action === "board" && boardProject && !UUID_PATTERN.test(boardProject))
|
|
116
137
|
throw new Error("board --project requires a uuid.");
|
|
@@ -119,7 +140,51 @@ export function parseIssueArgs(args) {
|
|
|
119
140
|
const assignee = readIssueAssignee(action, values);
|
|
120
141
|
const limit = readIssueLimit(action, values);
|
|
121
142
|
const versionNumber = readIssueVersion(action, values);
|
|
122
|
-
return buildIssueCommand(values, action, issueRef, issueRefs, moveState, stateFilter, title, priority, estimate, assignee, limit, versionNumber);
|
|
143
|
+
return buildIssueCommand(values, action, issueRef, issueRefs, moveState, stateFilter, title, priority, estimate, assignee, limit, versionNumber, page);
|
|
144
|
+
}
|
|
145
|
+
const INSTANT = /^\d{4}-\d\d-\d\d(?:T\d\d:\d\d:\d\d(?:\.\d{1,6})?(?:Z|[+-]\d\d:\d\d))?$/;
|
|
146
|
+
/** BLI-21315: the cursor contract's flags. Filters and sort belong to `list`; `--cursor` also to `history`. */
|
|
147
|
+
function readIssuePage(action, values) {
|
|
148
|
+
const named = ["--q", "--created-after", "--created-before", "--updated-after", "--updated-before", "--sort", "--dir"];
|
|
149
|
+
for (const flag of named) {
|
|
150
|
+
if (values.flags.has(flag) && action !== "list")
|
|
151
|
+
throw new Error(`${flag} belongs to \`cockpit ticket list\`.`);
|
|
152
|
+
}
|
|
153
|
+
if (values.booleans.has("--all") && action !== "list")
|
|
154
|
+
throw new Error("--all belongs to `cockpit ticket list`.");
|
|
155
|
+
if (values.flags.has("--cursor") && action !== "list" && action !== "history") {
|
|
156
|
+
throw new Error("--cursor belongs to `cockpit ticket list` and `cockpit ticket history`.");
|
|
157
|
+
}
|
|
158
|
+
const instants = {};
|
|
159
|
+
for (const flag of ["--created-after", "--created-before", "--updated-after", "--updated-before"]) {
|
|
160
|
+
const value = optionalNonEmpty(values.flags.get(flag));
|
|
161
|
+
if (value !== undefined && (!INSTANT.test(value) || !Number.isFinite(Date.parse(value)))) {
|
|
162
|
+
throw new Error(`${flag} must be a UTC instant like 2026-10-01T00:00:00Z or a date like 2026-10-01.`);
|
|
163
|
+
}
|
|
164
|
+
instants[flag] = value;
|
|
165
|
+
}
|
|
166
|
+
const sort = optionalNonEmpty(values.flags.get("--sort"));
|
|
167
|
+
if (sort !== undefined && sort !== "updated_at" && sort !== "created_at")
|
|
168
|
+
throw new Error("--sort must be updated_at or created_at.");
|
|
169
|
+
const dir = optionalNonEmpty(values.flags.get("--dir"));
|
|
170
|
+
if (dir !== undefined && dir !== "asc" && dir !== "desc")
|
|
171
|
+
throw new Error("--dir must be asc or desc.");
|
|
172
|
+
const all = values.booleans.has("--all");
|
|
173
|
+
const cursor = optionalNonEmpty(values.flags.get("--cursor"));
|
|
174
|
+
if (all && cursor)
|
|
175
|
+
throw new Error("--all walks from the start; drop --cursor, or pass --cursor without --all to read one page.");
|
|
176
|
+
const q = optionalNonEmpty(values.flags.get("--q"));
|
|
177
|
+
return {
|
|
178
|
+
...(cursor ? { cursor } : {}),
|
|
179
|
+
...(q ? { q } : {}),
|
|
180
|
+
...(instants["--created-after"] ? { createdAfter: instants["--created-after"] } : {}),
|
|
181
|
+
...(instants["--created-before"] ? { createdBefore: instants["--created-before"] } : {}),
|
|
182
|
+
...(instants["--updated-after"] ? { updatedAfter: instants["--updated-after"] } : {}),
|
|
183
|
+
...(instants["--updated-before"] ? { updatedBefore: instants["--updated-before"] } : {}),
|
|
184
|
+
...(sort ? { sort } : {}),
|
|
185
|
+
...(dir ? { dir } : {}),
|
|
186
|
+
...(all ? { all } : {}),
|
|
187
|
+
};
|
|
123
188
|
}
|
|
124
189
|
function readIssueAction(values) {
|
|
125
190
|
const first = values.positionals[0];
|
|
@@ -230,7 +295,7 @@ function readIssueLimit(action, values) {
|
|
|
230
295
|
}
|
|
231
296
|
return limit;
|
|
232
297
|
}
|
|
233
|
-
function buildIssueCommand(values, action, issueRef, issueRefs, moveState, stateFilter, title, priority, estimate, assignee, limit, versionNumber) {
|
|
298
|
+
function buildIssueCommand(values, action, issueRef, issueRefs, moveState, stateFilter, title, priority, estimate, assignee, limit, versionNumber, page) {
|
|
234
299
|
return {
|
|
235
300
|
kind: "issue",
|
|
236
301
|
requestId: optionalNonEmpty(values.flags.get("--request-id")),
|
|
@@ -247,6 +312,7 @@ function buildIssueCommand(values, action, issueRef, issueRefs, moveState, state
|
|
|
247
312
|
rows: optionalNonEmpty(values.flags.get("--rows")),
|
|
248
313
|
project: optionalNonEmpty(values.flags.get("--project")),
|
|
249
314
|
...(limit === undefined ? {} : { limit }),
|
|
315
|
+
...page,
|
|
250
316
|
...(versionNumber === undefined ? {} : { versionNumber }),
|
|
251
317
|
title,
|
|
252
318
|
...(priority === undefined ? {} : { priority }),
|
|
@@ -259,8 +325,8 @@ function buildIssueCommand(values, action, issueRef, issueRefs, moveState, state
|
|
|
259
325
|
}
|
|
260
326
|
export function parseProjectArgs(args) {
|
|
261
327
|
const values = parseNamedArgs(args, {
|
|
262
|
-
allowedFlags: ["--home", "--dashboard-url", "--archived", "--json"],
|
|
263
|
-
valueFlags: ["--home", "--dashboard-url"],
|
|
328
|
+
allowedFlags: ["--home", "--dashboard-url", "--archived", "--cursor", "--limit", "--json"],
|
|
329
|
+
valueFlags: ["--home", "--dashboard-url", "--cursor", "--limit"],
|
|
264
330
|
});
|
|
265
331
|
const first = values.positionals[0];
|
|
266
332
|
const action = (first === undefined ? "list" : first);
|
|
@@ -275,6 +341,8 @@ export function parseProjectArgs(args) {
|
|
|
275
341
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
276
342
|
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
277
343
|
includeArchived: values.booleans.has("--archived"),
|
|
344
|
+
...(optionalNonEmpty(values.flags.get("--cursor")) ? { cursor: optionalNonEmpty(values.flags.get("--cursor")) } : {}),
|
|
345
|
+
...(values.flags.has("--limit") ? { limit: optionalPositiveInteger(values.flags.get("--limit"), "--limit") } : {}),
|
|
278
346
|
json: values.booleans.has("--json"),
|
|
279
347
|
};
|
|
280
348
|
}
|
|
@@ -197,10 +197,10 @@ export const TOWER_OPS_COMMAND_HELP = [
|
|
|
197
197
|
" cockpit drills start open the next drill: the change or command, what it printed, and the question. The cause is NOT shown",
|
|
198
198
|
" cockpit drills answer --confidence <1-5> your answer on stdin (or --text), and how sure you are; then the real cause and the guard are shown",
|
|
199
199
|
" cockpit drills theme your theme and the choices",
|
|
200
|
-
" cockpit drills theme set <theme> [--play <t>] [--watch <t>] [--train <t>]
|
|
200
|
+
" cockpit drills theme set <theme> [--play <t>] [--watch <t>] [--train <t>] plain (the default: no theme), video-game, sports, movies-tv, music, anime, cooking, or anything you're into",
|
|
201
201
|
" cockpit drills review your own week: hits, wins, highs, lows, where you have stopped improving, what to practise",
|
|
202
|
-
"A drill replays a real mistake the team already paid for. You answer first, so a wrong answer you were sure about is the one you remember; it comes back 1, 3 and 7 days later.",
|
|
203
|
-
"Each of your top patterns is a boss: pass its drills, then a stretch of your own PRs without taking that hit again. There is no leaderboard; these are your own rows only.",
|
|
202
|
+
"A drill replays a real mistake the team already paid for, shown straight with no theme unless you pick one. You answer first, so a wrong answer you were sure about is the one you remember; it comes back 1, 3 and 7 days later.",
|
|
203
|
+
"Each of your top patterns is something to beat (a boss, if you pick a theme): pass its drills, then a stretch of your own PRs without taking that hit again. There is no leaderboard; these are your own rows only.",
|
|
204
204
|
"Anything that needs your hit history says so when it is not connected yet, instead of showing a zero.",
|
|
205
205
|
"--home <dir> and --dashboard-url <url> are for tests and staging.",
|
|
206
206
|
"--json writes one object to stdout (the door's body) and nothing else; a refusal is { ok: false, error, detail } and exits 1.",
|
|
@@ -47,7 +47,7 @@ export const TOWER_COMMAND_HELP = [
|
|
|
47
47
|
[
|
|
48
48
|
"docs",
|
|
49
49
|
[
|
|
50
|
-
"Usage: cockpit docs [list|tree|read <id|slug> [<id|slug>...]|read --parent <id> --bodies|create|update <id>|delete <id|slug> [<id|slug>...]|link <id|slug> <notion-page>|sync-status [<id|slug>]|export <id|slug> [--format md|pdf|docx|txt] [--out <path>]|mutation-status <request-uuid>] [flags]",
|
|
50
|
+
"Usage: cockpit docs [list|tree|read <id|slug> [<id|slug>...]|read --parent <id> --bodies|create|update <id>|delete <id|slug> [<id|slug>...]|link <id|slug> <notion-page>|sync-status [<id|slug>]|export <id|slug> [--format md|pdf|docx|txt] [--out <path>]|mutation-status <request-uuid>|share <id|slug> --add|--remove <email>|access <id|slug>] [flags]",
|
|
51
51
|
"",
|
|
52
52
|
"The Tower document library, typed. Bare `cockpit docs` lists every document you may read.",
|
|
53
53
|
"list [--parent <id>|root] [--query <text> [--word]] [--limit <n>] — every document you may see: id, visibility, body size, slug, title. Metadata only — a list never carries a body, so it stays cheap enough for an agent to call (--json was 604 KB for 155 documents before BLI-3737, and is now a few tens of KB). --json shape: { documents: [{ id, slug, title, visibility, parent_id, source, updated_at, body_chars }] }.",
|
|
@@ -67,20 +67,21 @@ export const TOWER_COMMAND_HELP = [
|
|
|
67
67
|
" --request-id <uuid> makes a retry apply once (minted and printed when absent). Reusing it with different input is request_id_reused. If the response is lost, run `cockpit docs mutation-status <uuid>`, then retry the SAME input with the SAME id.",
|
|
68
68
|
"mutation-status <request-uuid> — your own save receipt: committed (with the stored revision), not_committed (with the reason) or unknown_check_again (no receipt yet; it never proves nothing changed). --json shape: { mutation }.",
|
|
69
69
|
" --allow-empty clears the page on purpose. A body that would empty a document that holds text is refused (refused_empty_body) unless you say so, because far more often it is a surface that lost the content than a person who meant it.",
|
|
70
|
+
"access <id|slug> — who may do what on a page and why, in plain sentences (BLI-21313): your role (author, admin, granted_editor, reader), can_read/can_edit/can_manage, the named editors you may see (the owner and administrators see all; a named editor sees only their own grant), and the grant history for the owner and administrators. The same explanation the page, its versions and its export carry. --json shape: { ok, document_id, permission: { role, can_read, can_edit, can_manage, editors, editors_complete, reasons }, events }.",
|
|
71
|
+
"share <id|slug> --add <email> | --remove <email> [--reason \"<note>\"] — owner or administrator only. Names (or removes) an editor of an ORGANIZATION page by exact email, never fuzzy. An editor can change the title and text; they cannot change visibility, move, delete or manage editors, and nobody new gains the right to read. A page you made private refuses with private_page_grant_needs_decision (sharing a private page waits for a decision) and nothing is written. Other refusals: not_grant_manager, person_not_found, grantee_is_author, document_not_found_or_unreadable. Adding someone already named, or removing someone not named, answers outcome unchanged. --json shape: { ok, outcome, reason, document_id, grantee_id, request_id, permission }.",
|
|
70
72
|
"delete <id|slug> [<id|slug>...] — soft-deletes one or more documents (BLI-10555): the browser's own delete, sets deleted_at, never a real SQL DELETE. Recoverable — only a super_admin clearing deleted_at brings it back, there is no self-service undo. Deleting a parent does NOT delete its children; they keep their own parent_id and stay exactly where they were. Several refs resolve against ONE shared list call, then one DELETE each — one bad ref never loses the rest. --json shape: one ref answers { ok, document_id }; several answer { documents: [{ ref, ok, document_id } | { ref, ok: false, error, detail }] }.",
|
|
71
73
|
"link <id|slug> <notion-url|page-id> — ties the document to a Notion page so the two-way sync moves it both ways. The page is the truth from then on: the next sync pulls it over this copy and keeps the copy as a version. `link <id|slug> --clear` unties it. Refusals: invalid_notion_page, notion_page_already_linked, document_not_writable.",
|
|
72
74
|
"sync-status [<id|slug>] — every linked document's Notion sync state, or one's: synced (the body is what both sides last agreed on), tower_changed (edited here since; the next write-back pushes it), never_synced (linked, no baseline yet). Tower's half only: a Notion edit shows up at the next pull, not here. Metadata only, never a body.",
|
|
73
75
|
"export <id|slug> [--format md|pdf|docx|txt] [--out <path>] — writes the document as a file (ticket 20694): md is the stored Markdown exactly, txt is readable plain text, docx is a Word file with real headings, lists, tables and links, pdf is a paginated page with the title and last-updated date in the header. Default format md; default path is the document's slug plus the extension in the current directory (--out names a file, or a directory to put it in), and an existing file is replaced. Prints the path; --json shape: { ok, path, bytes, format }. To copy a page's text, `cockpit docs read` already does it.",
|
|
74
76
|
"A document body is never accepted on the command line — --file (safest on Windows) or a pipe only, same discipline as `cockpit notes paste`.",
|
|
75
|
-
"--json writes one machine-readable object to stdout; every reason and receipt line stays on stderr.",
|
|
76
|
-
"A refusal keeps Tower's own reason label — needs_rls_client, document_not_found_or_unreadable, document_not_writable, slug_taken, circular_parent, and so on.",
|
|
77
|
+
"--json writes one machine-readable object to stdout; every reason and receipt line stays on stderr. A refusal keeps Tower's own reason label — needs_rls_client, document_not_found_or_unreadable, document_not_writable, slug_taken, circular_parent, and so on.",
|
|
77
78
|
"The command uses the existing paired device identity. Run `cockpit login` first if this machine is not paired.",
|
|
78
79
|
],
|
|
79
80
|
],
|
|
80
81
|
[
|
|
81
82
|
"msg",
|
|
82
83
|
[
|
|
83
|
-
"Usage: cockpit msg [channels|create <name>|dm <email>|read <channel>|unread <channel> --from <message-id>|send <channel>|thread <id> --channel <channel>|show <message-id>] [flags]",
|
|
84
|
+
"Usage: cockpit msg [channels|create <name>|dm <email>|read <channel>|unread <channel> --from <message-id>|send <channel>|edit <message-id>|delete <message-id>|outcome <request-uuid>|thread <id> --channel <channel>|show <message-id>] [flags]",
|
|
84
85
|
"",
|
|
85
86
|
"Channels and messages, typed. <channel> is a channel id, or its name with or without a leading #.",
|
|
86
87
|
"channels — every channel you are a member of (or, as a super_admin, every channel).",
|
|
@@ -88,7 +89,8 @@ export const TOWER_COMMAND_HELP = [
|
|
|
88
89
|
"dm <email> — opens (or re-opens) the direct message with one person. Idempotent: the same address always resolves to the same channel, and you never have to name yourself.",
|
|
89
90
|
"read <channel> [--limit <n>] [--thread <id>] [--mark-read] — the channel's most recent top-level messages, oldest first; --thread <id> reads one thread's replies instead. The first page also says where the messages come from (for an imported Slack channel: how long ago the archive was checked, or that it is unknown) and where YOUR read line stands. Reading never moves the read line; --mark-read moves it to the newest message shown, and says plainly if that failed.",
|
|
90
91
|
"unread <channel> --from <message-id> — marks that message and everything after it unread, so the web sidebar badge and the NEW MESSAGES line start there. Opening or reading a channel never clears unseen history.",
|
|
91
|
-
"send <channel> [--thread <id>] — posts a message. The content is never accepted on the command line: pipe it in, e.g. `echo \"hello\" | cockpit msg send general`.",
|
|
92
|
+
"send <channel> [--thread <id>] [--request-id <uuid>] — posts a message. The content is never accepted on the command line: pipe it in, e.g. `echo \"hello\" | cockpit msg send general`. Every send carries a request id (minted when you give none); retry a send whose answer was lost with the SAME --request-id and the same text and it posts once. The same id with different text is refused request_id_reused, not applied as an edit.",
|
|
93
|
+
"edit <message-id> [--expected-revision <n>] [--request-id <uuid>] — replaces your own message's text (piped in). With --expected-revision, an edit of a message that has moved on is refused revision_conflict and nothing is saved over it; the current revision is printed. A message you did not write, or one that was deleted, is refused by name. delete <message-id> [--expected-revision <n>] [--request-id <uuid>] — deletes by tombstone: the text is cleared and every surface reads \"This message was deleted\", but the row stays so its thread, replies and reply count survive. Search and unread counts ignore it. A repeat answers already_deleted. outcome <request-uuid> — your own write receipt for a send, edit or delete: committed, not_committed (with the reason) or unknown_check_again. Run it after a timeout before retrying.",
|
|
92
94
|
"thread <id> --channel <channel> [--before <cursor>] [--limit <n>] — one thread's replies by the parent message's id.",
|
|
93
95
|
"show <message-id> — exact message, thread root and bounded neighbors. read/thread --before takes the preceding nextCursor; repeat until hasMore is false.",
|
|
94
96
|
"--json writes one machine-readable object to stdout; every reason and receipt line stays on stderr.",
|
|
@@ -102,7 +104,7 @@ export const TOWER_COMMAND_HELP = [
|
|
|
102
104
|
"Usage: cockpit issue [board|list|show <id> [<id>...]|create|update <id>|move <id> <state>|comment <id>|history <id>|versions <id>|restore <id> --version <n>|mutation-status <request-uuid>] [flags]",
|
|
103
105
|
"",
|
|
104
106
|
"Tower's issue tracker. <id> is a BLI-#### identifier (BLI-3654) or an issue's uuid — both work everywhere an issue is named.",
|
|
105
|
-
"list [--state <s>] [--assignee me|unassigned|<uuid>] [--project <name|id>] [--limit <n>] — the issues you may see, most recently updated first. Each row carries assignee_name/assignee_email alongside the bare assignee_id (BLI-10450, one extra `cockpit team members` call for the whole page, never per row), and a `size` label (S/M/L) alongside the raw `estimate` when it matches Tower's own scale (BLI-10575). --json shape: { issues: [{ id, identifier, title, state, priority, estimate, size, assignee_id, assignee_name, assignee_email, project_id, parent_id, updated_at }] }.",
|
|
107
|
+
"list [--state <s>] [--assignee me|unassigned|<uuid>] [--project <name|id>] [--label <name>] [--q <text>] [--created-after|--created-before|--updated-after|--updated-before <utc instant|date>] [--sort updated_at|created_at] [--dir desc|asc] [--limit <n>] [--cursor <c>] [--all] — the issues you may see, most recently updated first. A page is 100 by default and at most 500. Filters run in the database BEFORE the page, so a label or title match deep in the set is found. --q is a case-insensitive title contains (% and _ are literal); *-after is inclusive, *-before is exclusive. --json adds next_cursor, has_more, coverage (complete|capped|failed_partial), requested, returned, unique: pass next_cursor back as --cursor with the same other flags. --all walks every page and streams ONE JSON OBJECT PER LINE (NDJSON; rows carry assignee_id, no names), then a final {\"type\":\"coverage\",...} line naming the coverage; an interrupted walk ends with coverage failed_partial, the reason and the resume cursor, and exits 1. Each row carries assignee_name/assignee_email alongside the bare assignee_id (BLI-10450, one extra `cockpit team members` call for the whole page, never per row), and a `size` label (S/M/L) alongside the raw `estimate` when it matches Tower's own scale (BLI-10575). --json shape: { issues: [{ id, identifier, title, state, priority, estimate, size, assignee_id, assignee_name, assignee_email, project_id, parent_id, updated_at }] }.",
|
|
106
108
|
"board [--json] [--state <s>] [--assignee me|unassigned|<uuid>] [--project <uuid>] [--label <name>] [--rows <state:n,...>] — the Tickets board, counts, coverage and top five cards per column, through GET /api/work/board. Also cockpit tickets board.",
|
|
107
109
|
"show <id> [<id>...] — one or more issues: title, state, priority, size, assignee (name and email, not just its id), description, and every comment on each. Several ids read in one call — one bad id never loses the rest.",
|
|
108
110
|
" --json shape: one id answers { issue, comments }, unchanged from before BLI-10450 (plus `estimate`/`size`, BLI-10575). Several ids answer { issues: [{ ref, ok, issue, comments } | { ref, ok: false, error, detail }] }.",
|
|
@@ -112,7 +114,7 @@ export const TOWER_COMMAND_HELP = [
|
|
|
112
114
|
"comment <id> — posts a comment. The body is never taken on the command line: `echo \"shipped\" | cockpit issue comment BLI-3654`.",
|
|
113
115
|
"Writes accept --request-id <uuid> and --expected-revision <n>. Keep the UUID and identical fields across retries; JSON includes mutation receipt and mirror delivery separately.",
|
|
114
116
|
"mutation-status <request-uuid> — check your own local commit and mirror delivery after a lost response; no receipt means unknown_check_again.",
|
|
115
|
-
"history <id> [--limit <n>] — every recorded state move and reassignment, oldest first.",
|
|
117
|
+
"history <id> [--limit <n>] [--cursor <c>] — every recorded state move and reassignment, oldest first; --json carries the same page fields as list.",
|
|
116
118
|
"versions <id> [--limit <n>] — every saved description, newest first, each with its number, id, author, source and time (BLI-21300). --json shape: { versions: [{ id, version, markdown, author_id, source, restored_from_version, created_at }] }.",
|
|
117
119
|
"restore <id> --version <n> [--request-id <uuid>] [--expected-revision <n>] — writes version <n>'s text back as a NEW version; nothing is rewritten, so the restore can itself be undone. A stale revision conflicts and changes nothing. --json shape: { issue, mutation }.",
|
|
118
120
|
"A project may be named instead of id'd; the name is matched exactly (case-insensitively) against `cockpit project list`.",
|
|
@@ -151,7 +153,7 @@ export const TOWER_COMMAND_HELP = [
|
|
|
151
153
|
[
|
|
152
154
|
"cal",
|
|
153
155
|
[
|
|
154
|
-
'Usage: cockpit cal [event <id>|export <id>|today|week|conflicts|calendar-change <id> --color <hex-or-provider>|next|find "<words>"|calendars|add-ical|create|duplicate <event id>|batch|undo [<change id>]|share <id>|sync <id>|sync-status [<id>]|detach <id>|delete <event id> --yes] [flags]',
|
|
156
|
+
'Usage: cockpit cal [event <id>|export <id>|today|week|conflicts|calendar-change <id> --color <hex-or-provider>|next|find "<words>"|calendars|add-ical|create|duplicate <event id>|batch|undo [<change id>]|rsvp <event id> --response <accepted|declined|tentative>|change <event id>|cancel <event id> --yes|share <id>|sync <id>|sync-status [<id>]|detach <id>|delete <event id> --yes] [flags]',
|
|
155
157
|
"",
|
|
156
158
|
"Every calendar you attached, in one place — plus any calendar a colleague marked shared.",
|
|
157
159
|
"today [--offset 1] [--tz <zone>] — what is on today, in YOUR zone (this machine's, unless --tz says otherwise). --offset 1 is tomorrow, -1 yesterday.",
|
|
@@ -170,6 +172,8 @@ export const TOWER_COMMAND_HELP = [
|
|
|
170
172
|
" A work @buildlaunchiterate.ca calendar does not need this: connect Google in the browser and `cockpit cal calendars` will list them.",
|
|
171
173
|
'create [--account <name>|--calendar <id>] --title "<what>" --at <iso> --until <iso> [--location "<where>"] [--attendee a@x,b@y] [--all-day] [--meet] — creates the event AT GOOGLE, then here. Attendees are invited by Google; --meet asks for a Google Meet room and prints its address (or says meet_not_available and keeps the event).',
|
|
172
174
|
" Only a Google calendar can be written to: an iCal secret address is a read address, and `write_not_supported_for_provider` says so by name.",
|
|
175
|
+
"rsvp <event id> --response accepted|declined|tentative [--comment \"<text>\"] [--scope instance|series] [--occurrence <iso>] [--account <name>] [--key <k>] — answers YOUR invitation; only your own guest entry changes and nobody else is emailed by Tower. Refuses with not_a_guest when you are not on the guest list.",
|
|
176
|
+
"change <event id> [--title ..] [--at <iso>] [--until <iso>] [--location ..] [--scope instance|following|series] [--occurrence <iso>] [--color 1-11|none] [--reminder <minutes[:popup|email]>|default]... [--visibility default|public|private|confidential] [--transparency opaque|transparent] — a recurring event REQUIRES --scope (instance and following also need --occurrence): instance = this event, following = this and following events (starts a NEW series), series = all events. --reminder repeats, at most 5; `default` alone means the calendar default. Refusals keep the door's reason (scope_required, occurrence_required, scope_not_supported, following_is_series, write_refused). cancel <event id> --yes [--scope instance|following|series] [--occurrence <iso>] [--account <name>] — same scope rules as change; `following` trims the series and prints the new end. create also takes --color, --reminder, --visibility and --transparency.",
|
|
173
177
|
"share <id> --org-visible|--private — makes one of YOUR calendars readable by everyone at BLI, or private again. Sharing never lets anybody else change it.",
|
|
174
178
|
"sync <id> [--full] — reads that calendar now instead of waiting for the cron. --full ignores the sync token / ETag and re-reads everything.",
|
|
175
179
|
"sync-status [<id>] — every calendar's health, or one's: last successful sync, the last pass's own reason, how many events it carried, failures in the last 24h, and the lag between an event's own provider clock and when Tower stored it (ticket 10730).",
|
|
@@ -188,10 +192,10 @@ export const TOWER_COMMAND_HELP = [
|
|
|
188
192
|
[
|
|
189
193
|
"project",
|
|
190
194
|
[
|
|
191
|
-
"Usage: cockpit project [list] [--archived] [--json]",
|
|
195
|
+
"Usage: cockpit project [list] [--archived] [--limit <n>] [--cursor <c>] [--json]",
|
|
192
196
|
"",
|
|
193
197
|
"The projects issues are filed under. Bare `cockpit project` lists them.",
|
|
194
|
-
"list [--archived] — id, active/archived, name. --archived includes archived projects.",
|
|
198
|
+
"list [--archived] [--limit <n>] [--cursor <c>] — id, active/archived, name. --archived includes archived projects. A page is at most 500; when more exist the output (and --json: next_cursor, has_more, coverage, requested, returned, unique) carries the cursor to pass back as --cursor.",
|
|
195
199
|
"There is no create/update/delete verb: `GET /api/work/projects` is the whole of Tower's project door today.",
|
|
196
200
|
"--json writes one machine-readable object to stdout; every reason and receipt line stays on stderr.",
|
|
197
201
|
],
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The message WRITE verbs of `cockpit msg` (BLI-21314): `edit`, `delete`, `outcome`, plus what `send`
|
|
3
|
+
* shares with them (the piped body, the one deleted sentence, the refusal that says how to check a write).
|
|
4
|
+
* Split out of `msg.ts` to keep it under the file-size ratchet. Same doors the browser calls.
|
|
5
|
+
*/
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
8
|
+
import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
|
|
9
|
+
const TAG = "[msg cli]";
|
|
10
|
+
const READ_DEADLINE_MS = 30_000;
|
|
11
|
+
const WRITE_DEADLINE_MS = 30_000;
|
|
12
|
+
const CONTENT_MAX_CHARS = 8_000;
|
|
13
|
+
/** The one sentence a deleted message reads as, on every surface. */
|
|
14
|
+
export const DELETED_SENTENCE = "This message was deleted";
|
|
15
|
+
export function messageText(message) {
|
|
16
|
+
return message.deleted_at ? `(${DELETED_SENTENCE})` : (message.content ?? "");
|
|
17
|
+
}
|
|
18
|
+
export async function readContent(io) {
|
|
19
|
+
if (isInteractiveStdin(io)) {
|
|
20
|
+
return {
|
|
21
|
+
ok: false,
|
|
22
|
+
reason: "nothing_piped",
|
|
23
|
+
detail: "Pipe the message in, e.g. `echo \"hello\" | cockpit msg send general`. A message never travels on the command line.",
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const text = await readPipedText(io.stdin, {
|
|
28
|
+
maxChars: CONTENT_MAX_CHARS,
|
|
29
|
+
overflowMessage: `A message is limited to ${CONTENT_MAX_CHARS} characters.`,
|
|
30
|
+
});
|
|
31
|
+
return { ok: true, text: text.trim() };
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
return { ok: false, reason: "content_too_long", detail: error instanceof Error ? error.message : String(error) };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Says where the write stands, then fails with the door's own reason. A conflict names the current revision;
|
|
39
|
+
* an unknown result says how to resolve it by request id before retrying.
|
|
40
|
+
*/
|
|
41
|
+
export function refuseMsgWrite(door, answer, requestId, verb) {
|
|
42
|
+
const mutation = answer.body?.mutation;
|
|
43
|
+
writeLine(door.io.stderr, `${TAG} check the ${verb} with cockpit msg outcome ${requestId}; retry unchanged input with --request-id ${requestId}.`);
|
|
44
|
+
if (answer.reason === "revision_conflict") {
|
|
45
|
+
writeLine(door.io.stderr, `${TAG} conflict ${JSON.stringify({ request_id: requestId, current_revision: mutation?.current?.revision ?? mutation?.revision ?? null })}`);
|
|
46
|
+
}
|
|
47
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail, mutation ? { mutation } : {});
|
|
48
|
+
}
|
|
49
|
+
/** `cockpit msg edit <message-id> [--expected-revision <n>] [--request-id <uuid>]` — the new text is piped in. */
|
|
50
|
+
export async function editMessage(command, door) {
|
|
51
|
+
const content = await readContent(door.io);
|
|
52
|
+
if (!content.ok)
|
|
53
|
+
return failAgentDoor(door, TAG, content.reason, content.detail);
|
|
54
|
+
if (content.text === "")
|
|
55
|
+
return failAgentDoor(door, TAG, "invalid_body", "There was nothing to save. Nothing was changed.");
|
|
56
|
+
const requestId = command.requestId ?? randomUUID();
|
|
57
|
+
const answer = await askAgentDoor(door, {
|
|
58
|
+
path: `/api/msg/messages/${encodeURIComponent(command.targetId ?? "")}`,
|
|
59
|
+
method: "PATCH",
|
|
60
|
+
label: "msg edit",
|
|
61
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
62
|
+
body: {
|
|
63
|
+
content: content.text,
|
|
64
|
+
request_id: requestId,
|
|
65
|
+
...(command.expectedRevision === undefined ? {} : { expected_revision: command.expectedRevision }),
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
if (!answer.ok)
|
|
69
|
+
return refuseMsgWrite(door, answer, requestId, "edit");
|
|
70
|
+
const { message = null, mutation = null } = answer.body;
|
|
71
|
+
writeLine(door.io.stderr, `${TAG} edited ${JSON.stringify({ message_id: command.targetId, request_id: requestId, revision: mutation?.["revision"] ?? null })}`);
|
|
72
|
+
if (door.json)
|
|
73
|
+
return emitAgentDoor(door, { ok: true, message, mutation });
|
|
74
|
+
writeLine(door.io.stdout, `Edited ${message?.id ?? command.targetId}, revision ${String(mutation?.["revision"] ?? message?.revision ?? "?")}.`);
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
/** `cockpit msg delete <message-id> [--expected-revision <n>] [--request-id <uuid>]` — a tombstone: the thread stays, the text goes. */
|
|
78
|
+
export async function deleteMessage(command, door) {
|
|
79
|
+
const requestId = command.requestId ?? randomUUID();
|
|
80
|
+
const answer = await askAgentDoor(door, {
|
|
81
|
+
path: `/api/msg/messages/${encodeURIComponent(command.targetId ?? "")}`,
|
|
82
|
+
method: "DELETE",
|
|
83
|
+
label: "msg delete",
|
|
84
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
85
|
+
body: {
|
|
86
|
+
request_id: requestId,
|
|
87
|
+
...(command.expectedRevision === undefined ? {} : { expected_revision: command.expectedRevision }),
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
if (!answer.ok)
|
|
91
|
+
return refuseMsgWrite(door, answer, requestId, "delete");
|
|
92
|
+
const { message = null, mutation = null } = answer.body;
|
|
93
|
+
writeLine(door.io.stderr, `${TAG} deleted ${JSON.stringify({ message_id: command.targetId, request_id: requestId, reason: mutation?.["reason"] ?? "ok" })}`);
|
|
94
|
+
if (door.json)
|
|
95
|
+
return emitAgentDoor(door, { ok: true, message, mutation });
|
|
96
|
+
writeLine(door.io.stdout, `Deleted ${command.targetId} (${DELETED_SENTENCE.toLowerCase()}; its thread and replies stay).`);
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
/** `cockpit msg outcome <request-uuid>` — your own write receipt. No receipt is `unknown_check_again`, never "nothing changed". */
|
|
100
|
+
export async function messageOutcome(command, door) {
|
|
101
|
+
const answer = await askAgentDoor(door, {
|
|
102
|
+
path: `/api/msg/mutations/${encodeURIComponent(command.targetId ?? "")}`,
|
|
103
|
+
method: "GET",
|
|
104
|
+
label: "msg outcome",
|
|
105
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
106
|
+
});
|
|
107
|
+
if (!answer.ok)
|
|
108
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
109
|
+
const mutation = answer.body.mutation ?? {};
|
|
110
|
+
writeLine(door.io.stderr, `${TAG} outcome ${JSON.stringify({ request_id: command.targetId, outcome: mutation["outcome"] ?? null })}`);
|
|
111
|
+
if (door.json)
|
|
112
|
+
return emitAgentDoor(door, { ok: true, mutation });
|
|
113
|
+
const outcome = String(mutation["outcome"] ?? "unknown_check_again");
|
|
114
|
+
writeLine(door.io.stdout, outcome === "unknown_check_again"
|
|
115
|
+
? "Unknown: Tower has no receipt for that request yet. It may still be in flight; check again before retrying, and retry with the same --request-id."
|
|
116
|
+
: `${outcome}${mutation["reason"] ? ` (${String(mutation["reason"])})` : ""}${mutation["message_id"] ? `, message ${String(mutation["message_id"])}` : ""}.`);
|
|
117
|
+
return 0;
|
|
118
|
+
}
|