@bli-cockpit/cli 0.2.238 → 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-direct.js +119 -0
- package/dist/commands/cal-scope.js +71 -0
- package/dist/commands/cal.js +33 -2
- package/dist/commands/docs-share.js +81 -0
- package/dist/commands/docs.js +43 -6
- 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-versions.js +93 -0
- package/dist/commands/issue.js +28 -5
- package/dist/commands/local-args-tower-cal.js +113 -9
- package/dist/commands/local-args-tower-docs-msg.js +52 -7
- package/dist/commands/local-args-tower-drills.js +1 -1
- package/dist/commands/local-args-tower-msg.js +62 -7
- package/dist/commands/local-args-tower-setup.js +11 -4
- package/dist/commands/local-args-tower-work.js +97 -11
- package/dist/commands/local-help-commands-tower-ops.js +3 -3
- package/dist/commands/local-help-commands-tower.js +27 -14
- package/dist/commands/local-help-commands.js +1 -1
- package/dist/commands/local-help.js +2 -2
- package/dist/commands/msg-read-line.js +32 -0
- package/dist/commands/msg-write.js +118 -0
- package/dist/commands/msg.js +64 -31
- package/dist/commands/project.js +15 -3
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/setup.js +13 -0
- package/dist/commands/usage.js +3 -1
- package/dist/spool/local-spool-parse.js +13 -12
- package/package.json +4 -4
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
2
|
-
const
|
|
3
|
-
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"]);
|
|
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
|
|
6
7
|
* fuzzy (BLI-3749). The door resolves each one exactly, so a typo comes back
|
|
@@ -35,24 +36,73 @@ export function parseMsgArgs(args) {
|
|
|
35
36
|
"--private",
|
|
36
37
|
"--members",
|
|
37
38
|
"--description",
|
|
39
|
+
"--from",
|
|
40
|
+
"--mark-read",
|
|
41
|
+
"--request-id",
|
|
42
|
+
"--expected-revision",
|
|
38
43
|
"--json",
|
|
39
44
|
],
|
|
40
|
-
valueFlags: [
|
|
45
|
+
valueFlags: [
|
|
46
|
+
"--home", "--dashboard-url", "--channel", "--thread", "--limit", "--members", "--description", "--before", "--from",
|
|
47
|
+
"--request-id", "--expected-revision",
|
|
48
|
+
],
|
|
41
49
|
});
|
|
42
50
|
const { action, rest } = parseMsgAction(values.positionals);
|
|
43
51
|
if (values.flags.has("--before") && action !== "read" && action !== "thread") {
|
|
44
52
|
throw new Error("--before belongs to msg read or msg thread.");
|
|
45
53
|
}
|
|
54
|
+
const { markRead, unreadFrom } = parseMsgReadLineFlags(action, values);
|
|
46
55
|
const { channelName, dmEmail } = parseMsgCreateTarget(action, rest);
|
|
47
56
|
const { isPrivate, memberEmails, description } = parseMsgCreateFlags(action, values);
|
|
48
57
|
const { channelRef, threadId, limit } = parseMsgReadTarget(action, rest, values);
|
|
49
|
-
|
|
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 };
|
|
87
|
+
}
|
|
88
|
+
/** `read --mark-read` and `unread --from <message-id>` (BLI-21299): the read line, moved on purpose. */
|
|
89
|
+
function parseMsgReadLineFlags(action, values) {
|
|
90
|
+
const markRead = values.booleans.has("--mark-read");
|
|
91
|
+
if (markRead && action !== "read")
|
|
92
|
+
throw new Error("--mark-read belongs to `cockpit msg read`.");
|
|
93
|
+
const unreadFrom = optionalNonEmpty(values.flags.get("--from"));
|
|
94
|
+
if (action === "unread" && !unreadFrom) {
|
|
95
|
+
throw new Error("msg unread needs --from <message-id>, e.g. `cockpit msg unread general --from <message-id>`.");
|
|
96
|
+
}
|
|
97
|
+
if (unreadFrom && action !== "unread")
|
|
98
|
+
throw new Error("--from belongs to `cockpit msg unread`.");
|
|
99
|
+
return { markRead, unreadFrom };
|
|
50
100
|
}
|
|
51
101
|
function parseMsgAction(positionals) {
|
|
52
102
|
const first = positionals[0];
|
|
53
103
|
const action = (first === undefined ? "channels" : first);
|
|
54
104
|
if (!MSG_ACTIONS.has(action)) {
|
|
55
|
-
throw new Error(`Unknown msg command: ${first}. Try channels, read, 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.`);
|
|
56
106
|
}
|
|
57
107
|
return { action, rest: positionals.slice(first === undefined ? 0 : 1) };
|
|
58
108
|
}
|
|
@@ -113,7 +163,7 @@ function parseMsgReadTarget(action, rest, values) {
|
|
|
113
163
|
if (rest.length > 1)
|
|
114
164
|
throw new Error(`msg thread takes one thread id, not ${rest.length}.`);
|
|
115
165
|
}
|
|
116
|
-
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) {
|
|
117
167
|
throw new Error(`msg ${action} does not take "${rest[0]}".`);
|
|
118
168
|
}
|
|
119
169
|
const threadFlag = optionalNonEmpty(values.flags.get("--thread"));
|
|
@@ -129,7 +179,7 @@ function parseMsgReadTarget(action, rest, values) {
|
|
|
129
179
|
}
|
|
130
180
|
return { channelRef, threadId, limit };
|
|
131
181
|
}
|
|
132
|
-
function buildMsgCommand(values, action, channelRef, channelName, dmEmail, memberEmails, description, isPrivate, threadId, limit) {
|
|
182
|
+
function buildMsgCommand(values, action, channelRef, channelName, dmEmail, memberEmails, description, isPrivate, threadId, limit, markRead, unreadFrom, writeSafety) {
|
|
133
183
|
return {
|
|
134
184
|
kind: "msg",
|
|
135
185
|
action,
|
|
@@ -144,6 +194,11 @@ function buildMsgCommand(values, action, channelRef, channelName, dmEmail, membe
|
|
|
144
194
|
isPrivate,
|
|
145
195
|
...(threadId ? { threadId } : {}),
|
|
146
196
|
...(limit === undefined ? {} : { limit }),
|
|
197
|
+
...(markRead ? { markRead } : {}),
|
|
198
|
+
...(unreadFrom ? { unreadFrom } : {}),
|
|
199
|
+
...(writeSafety.targetId ? { targetId: writeSafety.targetId } : {}),
|
|
200
|
+
...(writeSafety.requestId ? { requestId: writeSafety.requestId } : {}),
|
|
201
|
+
...(writeSafety.expectedRevision === undefined ? {} : { expectedRevision: writeSafety.expectedRevision }),
|
|
147
202
|
json: values.booleans.has("--json"),
|
|
148
203
|
};
|
|
149
204
|
}
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
2
2
|
export function parseSetupArgs(args) {
|
|
3
|
-
const values = parseNamedArgs(args, { allowedFlags: ["--person", "--compare", "--since", "--details", "--json", "--home", "--dashboard-url"], valueFlags: ["--since", "--person", "--compare", "--home", "--dashboard-url"] });
|
|
3
|
+
const values = parseNamedArgs(args, { allowedFlags: ["--until", "--machines", "--person", "--compare", "--since", "--details", "--json", "--home", "--dashboard-url"], valueFlags: ["--until", "--since", "--person", "--compare", "--home", "--dashboard-url"] });
|
|
4
4
|
if (values.positionals.length)
|
|
5
5
|
throw new Error("setup takes --person or --compare, not a subcommand.");
|
|
6
6
|
const compare = optionalNonEmpty(values.flags.get("--compare"));
|
|
7
7
|
if (compare && (compare.split(",").length !== 2 || compare.split(",").some(value => !value.trim())))
|
|
8
8
|
throw new Error("--compare takes two people (first names or emails) separated by a comma.");
|
|
9
9
|
const since = optionalNonEmpty(values.flags.get("--since"));
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
const until = optionalNonEmpty(values.flags.get("--until"));
|
|
11
|
+
const validDate = (value) => /^\d{4}-\d{2}-\d{2}(?:T.*)?$/.test(value) && Number.isFinite(Date.parse(value)) && new Date(`${value.slice(0, 10)}T00:00:00Z`).toISOString().slice(0, 10) === value.slice(0, 10);
|
|
12
|
+
const end = until ? Date.parse(until) : Date.now();
|
|
13
|
+
const duration = since ? /^([1-9]\d*)([dh])$/.exec(since) : null;
|
|
14
|
+
const start = duration ? end - Number(duration[1]) * (duration[2] === "d" ? 86400000 : 3600000) : since ? Date.parse(since) : end - 30 * 86400000;
|
|
15
|
+
if (!Number.isFinite(end) || !Number.isFinite(start) || start >= end || (until && !validDate(until)) ||
|
|
16
|
+
(duration && Number(duration[1]) > (duration[2] === "d" ? 365 : 8760)) ||
|
|
17
|
+
(since && !duration && !validDate(since)))
|
|
18
|
+
throw new Error("--since takes a duration (1d, 7d, 30d, 90d) or an absolute start; --until takes an absolute end after the start.");
|
|
19
|
+
return { kind: "setup", since, until, machines: values.booleans.has("--machines"), details: values.booleans.has("--details"), person: optionalNonEmpty(values.flags.get("--person")), compare, json: values.booleans.has("--json"), homeDir: optionalNonEmpty(values.flags.get("--home")), dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")) };
|
|
13
20
|
}
|
|
@@ -27,6 +27,8 @@ const ISSUE_ACTIONS = new Set([
|
|
|
27
27
|
"move",
|
|
28
28
|
"comment",
|
|
29
29
|
"history",
|
|
30
|
+
"versions",
|
|
31
|
+
"restore",
|
|
30
32
|
]);
|
|
31
33
|
/** Every verb except `list` and `create` names ONE issue first. */
|
|
32
34
|
const ISSUE_ACTIONS_NEEDING_AN_ISSUE = new Set([
|
|
@@ -36,6 +38,8 @@ const ISSUE_ACTIONS_NEEDING_AN_ISSUE = new Set([
|
|
|
36
38
|
"move",
|
|
37
39
|
"comment",
|
|
38
40
|
"history",
|
|
41
|
+
"versions",
|
|
42
|
+
"restore",
|
|
39
43
|
]);
|
|
40
44
|
/**
|
|
41
45
|
* The closed state vocabulary, copied verbatim from
|
|
@@ -63,6 +67,16 @@ const ISSUE_ALLOWED_FLAGS = [
|
|
|
63
67
|
"--size",
|
|
64
68
|
"--parent",
|
|
65
69
|
"--file",
|
|
70
|
+
"--version",
|
|
71
|
+
"--cursor",
|
|
72
|
+
"--q",
|
|
73
|
+
"--created-after",
|
|
74
|
+
"--created-before",
|
|
75
|
+
"--updated-after",
|
|
76
|
+
"--updated-before",
|
|
77
|
+
"--sort",
|
|
78
|
+
"--dir",
|
|
79
|
+
"--all",
|
|
66
80
|
"--body-stdin",
|
|
67
81
|
"--json",
|
|
68
82
|
];
|
|
@@ -82,6 +96,15 @@ const ISSUE_VALUE_FLAGS = [
|
|
|
82
96
|
"--size",
|
|
83
97
|
"--parent",
|
|
84
98
|
"--file",
|
|
99
|
+
"--version",
|
|
100
|
+
"--cursor",
|
|
101
|
+
"--q",
|
|
102
|
+
"--created-after",
|
|
103
|
+
"--created-before",
|
|
104
|
+
"--updated-after",
|
|
105
|
+
"--updated-before",
|
|
106
|
+
"--sort",
|
|
107
|
+
"--dir",
|
|
85
108
|
];
|
|
86
109
|
export function parseIssueArgs(args) {
|
|
87
110
|
const values = parseNamedArgs(args, {
|
|
@@ -96,15 +119,19 @@ export function parseIssueArgs(args) {
|
|
|
96
119
|
throw new Error("--request-id must be a UUID.");
|
|
97
120
|
if (action === "mutation-status" && !UUID_PATTERN.test(issueRef ?? ""))
|
|
98
121
|
throw new Error("mutation-status needs a request UUID.");
|
|
99
|
-
if ((requestId || expectedRevision !== undefined) && !["create", "update", "move", "comment"].includes(action)) {
|
|
100
|
-
throw new Error("Write-safety flags belong to create, update, move or
|
|
122
|
+
if ((requestId || expectedRevision !== undefined) && !["create", "update", "move", "comment", "restore"].includes(action)) {
|
|
123
|
+
throw new Error("Write-safety flags belong to create, update, move, comment or restore.");
|
|
101
124
|
}
|
|
102
125
|
validateIssueState(moveState, "issue move state must be one of: ");
|
|
103
126
|
const stateFilter = readIssueStateFilter(action, values);
|
|
104
127
|
const title = readIssueTitle(action, values);
|
|
105
|
-
if (action !== "board" &&
|
|
106
|
-
throw new Error("--
|
|
128
|
+
if (action !== "board" && values.flags.has("--rows")) {
|
|
129
|
+
throw new Error("--rows widens cockpit tickets board only.");
|
|
107
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);
|
|
108
135
|
const boardProject = optionalNonEmpty(values.flags.get("--project"));
|
|
109
136
|
if (action === "board" && boardProject && !UUID_PATTERN.test(boardProject))
|
|
110
137
|
throw new Error("board --project requires a uuid.");
|
|
@@ -112,13 +139,58 @@ export function parseIssueArgs(args) {
|
|
|
112
139
|
const estimate = readIssueSize(values);
|
|
113
140
|
const assignee = readIssueAssignee(action, values);
|
|
114
141
|
const limit = readIssueLimit(action, values);
|
|
115
|
-
|
|
142
|
+
const versionNumber = readIssueVersion(action, values);
|
|
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
|
+
};
|
|
116
188
|
}
|
|
117
189
|
function readIssueAction(values) {
|
|
118
190
|
const first = values.positionals[0];
|
|
119
191
|
const action = (first === undefined ? "list" : first);
|
|
120
192
|
if (!ISSUE_ACTIONS.has(action)) {
|
|
121
|
-
throw new Error(`Unknown issue command: ${first}. Try board, list, show, create, update, move, comment, history, or mutation-status.`);
|
|
193
|
+
throw new Error(`Unknown issue command: ${first}. Try board, list, show, create, update, move, comment, history, versions, restore, or mutation-status.`);
|
|
122
194
|
}
|
|
123
195
|
return { action, rest: values.positionals.slice(first === undefined ? 0 : 1) };
|
|
124
196
|
}
|
|
@@ -206,14 +278,24 @@ function readIssueAssignee(action, values) {
|
|
|
206
278
|
}
|
|
207
279
|
return assignee;
|
|
208
280
|
}
|
|
281
|
+
/** `restore` names the version it wants; there is no "restore the previous one" (BLI-21300). */
|
|
282
|
+
function readIssueVersion(action, values) {
|
|
283
|
+
const version = optionalPositiveInteger(values.flags.get("--version"), "--version");
|
|
284
|
+
if (action === "restore" && version === undefined) {
|
|
285
|
+
throw new Error("issue restore needs --version <n>; list them with `cockpit ticket versions <id>`.");
|
|
286
|
+
}
|
|
287
|
+
if (version !== undefined && action !== "restore")
|
|
288
|
+
throw new Error("--version belongs to `cockpit ticket restore`.");
|
|
289
|
+
return version;
|
|
290
|
+
}
|
|
209
291
|
function readIssueLimit(action, values) {
|
|
210
292
|
const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
|
|
211
|
-
if (limit !== undefined && action !== "list" && action !== "history") {
|
|
212
|
-
throw new Error("--limit belongs to `cockpit issue list` or `
|
|
293
|
+
if (limit !== undefined && action !== "list" && action !== "history" && action !== "versions") {
|
|
294
|
+
throw new Error("--limit belongs to `cockpit issue list`, `history` or `versions`.");
|
|
213
295
|
}
|
|
214
296
|
return limit;
|
|
215
297
|
}
|
|
216
|
-
function buildIssueCommand(values, action, issueRef, issueRefs, moveState, stateFilter, title, priority, estimate, assignee, limit) {
|
|
298
|
+
function buildIssueCommand(values, action, issueRef, issueRefs, moveState, stateFilter, title, priority, estimate, assignee, limit, versionNumber, page) {
|
|
217
299
|
return {
|
|
218
300
|
kind: "issue",
|
|
219
301
|
requestId: optionalNonEmpty(values.flags.get("--request-id")),
|
|
@@ -230,6 +312,8 @@ function buildIssueCommand(values, action, issueRef, issueRefs, moveState, state
|
|
|
230
312
|
rows: optionalNonEmpty(values.flags.get("--rows")),
|
|
231
313
|
project: optionalNonEmpty(values.flags.get("--project")),
|
|
232
314
|
...(limit === undefined ? {} : { limit }),
|
|
315
|
+
...page,
|
|
316
|
+
...(versionNumber === undefined ? {} : { versionNumber }),
|
|
233
317
|
title,
|
|
234
318
|
...(priority === undefined ? {} : { priority }),
|
|
235
319
|
...(estimate === undefined ? {} : { estimate }),
|
|
@@ -241,8 +325,8 @@ function buildIssueCommand(values, action, issueRef, issueRefs, moveState, state
|
|
|
241
325
|
}
|
|
242
326
|
export function parseProjectArgs(args) {
|
|
243
327
|
const values = parseNamedArgs(args, {
|
|
244
|
-
allowedFlags: ["--home", "--dashboard-url", "--archived", "--json"],
|
|
245
|
-
valueFlags: ["--home", "--dashboard-url"],
|
|
328
|
+
allowedFlags: ["--home", "--dashboard-url", "--archived", "--cursor", "--limit", "--json"],
|
|
329
|
+
valueFlags: ["--home", "--dashboard-url", "--cursor", "--limit"],
|
|
246
330
|
});
|
|
247
331
|
const first = values.positionals[0];
|
|
248
332
|
const action = (first === undefined ? "list" : first);
|
|
@@ -257,6 +341,8 @@ export function parseProjectArgs(args) {
|
|
|
257
341
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
258
342
|
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
259
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") } : {}),
|
|
260
346
|
json: values.booleans.has("--json"),
|
|
261
347
|
};
|
|
262
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>]] [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 }] }.",
|
|
@@ -62,29 +62,35 @@ export const TOWER_COMMAND_HELP = [
|
|
|
62
62
|
" --json shape: one ref answers { document }, unchanged from before BLI-10450. Several refs answer { documents: [{ ref, ok, document } | { ref, ok: false, error, detail }] }.",
|
|
63
63
|
"read --parent <id>|root --bodies [--limit <n>] — every document filed directly under that parent, bodies included, in ONE call (GET /api/docs/documents?parent_id=...&bodies=true). --limit here caps the DOCUMENT COUNT (1-50), not characters. --json shape: { documents: [{ id, ..., body_markdown }] }.",
|
|
64
64
|
"create --title \"<title>\" [--parent <id>] [--visibility org|private] [--file <path>|--body-stdin] — the body comes from --file, or stdin (`cat body.md | cockpit docs create --title \"...\"`); neither means an empty body, matching the browser's own default. --json shape: { document }.",
|
|
65
|
-
"update <id|slug> [--title \"<t>\"] [--visibility org|private] [--parent <id>|--clear-parent] [--file <path>|--body-stdin] [--allow-empty] — only the fields you pass change; a `--parent` change IS a move, there is no separate move verb. --json shape: { document }.",
|
|
65
|
+
"update <id|slug> [--title \"<t>\"] [--visibility org|private] [--parent <id>|--clear-parent] [--file <path>|--body-stdin] [--allow-empty] [--expected-revision <n>] [--request-id <uuid>] — only the fields you pass change; a `--parent` change IS a move, there is no separate move verb. --json shape: { document, mutation }.",
|
|
66
|
+
" --expected-revision <n> is the page revision you read (`docs read --json` shows document.revision). The save is one atomic step: if someone saved since, nothing is written and you get revision_conflict with the current page (--json: mutation.current). Omit it for an unconditional save. Every save advances the revision.",
|
|
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
|
+
"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 }.",
|
|
66
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 }.",
|
|
67
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 }] }.",
|
|
68
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.",
|
|
69
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.",
|
|
70
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.",
|
|
71
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`.",
|
|
72
|
-
"--json writes one machine-readable object to stdout; every reason and receipt line stays on stderr.",
|
|
73
|
-
"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.",
|
|
74
78
|
"The command uses the existing paired device identity. Run `cockpit login` first if this machine is not paired.",
|
|
75
79
|
],
|
|
76
80
|
],
|
|
77
81
|
[
|
|
78
82
|
"msg",
|
|
79
83
|
[
|
|
80
|
-
"Usage: cockpit msg [channels|create <name>|dm <email>|read <channel>|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]",
|
|
81
85
|
"",
|
|
82
86
|
"Channels and messages, typed. <channel> is a channel id, or its name with or without a leading #.",
|
|
83
87
|
"channels — every channel you are a member of (or, as a super_admin, every channel).",
|
|
84
88
|
"create <name> [--private] [--members a@x.test,b@y.test] [--description \"<text>\"] — makes a channel and prints its id. A leading # is fine; --private means membership decides who may read it, and --members names who joins at birth BY EMAIL. An address Tower does not carry refuses the whole create — no half-built channel.",
|
|
85
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.",
|
|
86
|
-
"read <channel> [--limit <n>] [--thread <id>] — the channel's most recent top-level messages, oldest first; --thread <id> reads one thread's replies instead.",
|
|
87
|
-
"
|
|
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.",
|
|
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.",
|
|
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.",
|
|
88
94
|
"thread <id> --channel <channel> [--before <cursor>] [--limit <n>] — one thread's replies by the parent message's id.",
|
|
89
95
|
"show <message-id> — exact message, thread root and bounded neighbors. read/thread --before takes the preceding nextCursor; repeat until hasMore is false.",
|
|
90
96
|
"--json writes one machine-readable object to stdout; every reason and receipt line stays on stderr.",
|
|
@@ -95,10 +101,10 @@ export const TOWER_COMMAND_HELP = [
|
|
|
95
101
|
[
|
|
96
102
|
"issue",
|
|
97
103
|
[
|
|
98
|
-
"Usage: cockpit issue [board|list|show <id> [<id>...]|create|update <id>|move <id> <state>|comment <id>|history <id>|mutation-status <request-uuid>] [flags]",
|
|
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]",
|
|
99
105
|
"",
|
|
100
106
|
"Tower's issue tracker. <id> is a BLI-#### identifier (BLI-3654) or an issue's uuid — both work everywhere an issue is named.",
|
|
101
|
-
"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 }] }.",
|
|
102
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.",
|
|
103
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.",
|
|
104
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 }] }.",
|
|
@@ -108,7 +114,9 @@ export const TOWER_COMMAND_HELP = [
|
|
|
108
114
|
"comment <id> — posts a comment. The body is never taken on the command line: `echo \"shipped\" | cockpit issue comment BLI-3654`.",
|
|
109
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.",
|
|
110
116
|
"mutation-status <request-uuid> — check your own local commit and mirror delivery after a lost response; no receipt means unknown_check_again.",
|
|
111
|
-
"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.",
|
|
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 }] }.",
|
|
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 }.",
|
|
112
120
|
"A project may be named instead of id'd; the name is matched exactly (case-insensitively) against `cockpit project list`.",
|
|
113
121
|
"--json writes one machine-readable object to stdout; every reason and receipt line stays on stderr.",
|
|
114
122
|
"A refusal keeps Tower's own reason label — needs_rls_client, issue_not_found_or_unreadable, issue_not_writable, invalid_state, comment_too_long, and so on.",
|
|
@@ -145,7 +153,7 @@ export const TOWER_COMMAND_HELP = [
|
|
|
145
153
|
[
|
|
146
154
|
"cal",
|
|
147
155
|
[
|
|
148
|
-
'Usage: cockpit cal [event <id>|export <id>|today|week|conflicts|calendar-change <id> --color <hex-or-provider>|next|find "<words>"|calendars|add-ical|create|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]',
|
|
149
157
|
"",
|
|
150
158
|
"Every calendar you attached, in one place — plus any calendar a colleague marked shared.",
|
|
151
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.",
|
|
@@ -164,11 +172,16 @@ export const TOWER_COMMAND_HELP = [
|
|
|
164
172
|
" A work @buildlaunchiterate.ca calendar does not need this: connect Google in the browser and `cockpit cal calendars` will list them.",
|
|
165
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).',
|
|
166
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.",
|
|
167
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.",
|
|
168
178
|
"sync <id> [--full] — reads that calendar now instead of waiting for the cron. --full ignores the sync token / ETag and re-reads everything.",
|
|
169
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).",
|
|
170
180
|
"detach <id> — removes the calendar and its stored events from Tower. The calendar itself is untouched at Google.",
|
|
171
|
-
"change <event id> [--account <name>] [--title <title>] [--at <iso> --until <iso>] [--location <place>] — changes an event on its own account.",
|
|
181
|
+
"change <event id> [--account <name>] [--title <title>] [--at <iso> --until <iso>] [--location <place>] [--all-day|--timed] [--tz <zone>] [--expected-version <v>] [--idempotency-key <k>] [--reason <why>] — changes an event on its own account and prints the change id (undo it with `undo`). --all-day converts to an all-day event (--at/--until are dates, --until the day AFTER the last day); --timed converts back. --expected-version refuses (version_conflict) instead of overwriting an event that changed since you read it. Recurring events are refused until scoped edits ship.",
|
|
182
|
+
"duplicate <event id> [--at <iso> --until <iso>] [--calendar <id>|--account <name>] [--keep-invitees] [--idempotency-key <k>] — copies ONE non-recurring event. The copy has NO guests unless you pass --keep-invitees (Google then emails each one) and no Meet room. Same times unless --at/--until are given; --calendar/--account puts the copy on another of your calendars on purpose. The same --idempotency-key twice makes one copy.",
|
|
183
|
+
'batch [--idempotency-key <k>] [--reason <why>] — up to 25 changes from JSON on stdin: {"changes":[{"event_id":"…","starts_at":"…","ends_at":"…","expected_version":"…"}]} (or a bare array; fields as `change`: title, location, all_day, time_zone). Each is its own write: the output lists every item and the ones left unchanged, and the exit code is 1 unless all were written.',
|
|
184
|
+
"undo [<change id>] — a NEW write that puts the recorded earlier values back (or removes the event a create or duplicate made), never a deletion of history. No id means your latest change. Refuses when the event changed again since (undo_stale), when that change was already undone, and for guest changes, cancellations and calendar moves.",
|
|
172
185
|
"cancel <event id> --yes [--account <name>] — same door as delete; a named account must own the event.",
|
|
173
186
|
"delete <event id> --yes — removes ONE event, AT GOOGLE first, then here. Refuses without --yes: a delete can email everybody on it, exactly like create's invitations do. Google already having it gone (404/410) is answered as success, not a refusal.",
|
|
174
187
|
"--json writes one machine-readable object to stdout; every reason and receipt line stays on stderr.",
|
|
@@ -179,10 +192,10 @@ export const TOWER_COMMAND_HELP = [
|
|
|
179
192
|
[
|
|
180
193
|
"project",
|
|
181
194
|
[
|
|
182
|
-
"Usage: cockpit project [list] [--archived] [--json]",
|
|
195
|
+
"Usage: cockpit project [list] [--archived] [--limit <n>] [--cursor <c>] [--json]",
|
|
183
196
|
"",
|
|
184
197
|
"The projects issues are filed under. Bare `cockpit project` lists them.",
|
|
185
|
-
"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.",
|
|
186
199
|
"There is no create/update/delete verb: `GET /api/work/projects` is the whole of Tower's project door today.",
|
|
187
200
|
"--json writes one machine-readable object to stdout; every reason and receipt line stays on stderr.",
|
|
188
201
|
],
|
|
@@ -28,7 +28,7 @@ const COMMAND_HELP = new Map([
|
|
|
28
28
|
// BLI-3709: the Tower nouns live next door, split along the same seam the
|
|
29
29
|
// argument parsers already use — see `local-help-commands-tower.ts`.
|
|
30
30
|
...TOWER_COMMAND_HELP,
|
|
31
|
-
["setup", ["Usage: cockpit setup [--since <
|
|
31
|
+
["setup", ["Usage: cockpit setup [--since <duration|date>] [--until <date>] [--machines] [--details] [--person <name|email>] [--compare <name,name>] [--json]", "Read agent setups beside windowed outcomes (default 30 days), or compare two people by first name or email."]],
|
|
32
32
|
[
|
|
33
33
|
"onboard",
|
|
34
34
|
[
|
|
@@ -144,7 +144,7 @@ const COMMAND_SUMMARY_LINES = [
|
|
|
144
144
|
" cockpit msg [channels|create <name>|dm <email>|read <channel>|send <channel>|thread <id> --channel <channel>|show <message-id>] [--private] [--members a@x,b@y] [--description <text>] [--thread <id>] [--limit <n>] [--before <cursor>] [--dashboard-url <url>] [--json]",
|
|
145
145
|
" cockpit issue [list|show <BLI-id>|create --title <t>|update <BLI-id>|move <BLI-id> <state>|comment <BLI-id>|history <BLI-id>] [--state <s>] [--assignee me|unassigned|<uuid>] [--project <name|id>] [--limit <n>] [--priority 0-4] [--size S|M|L] [--parent <BLI-id>] [--file <path>|--body-stdin] [--dashboard-url <url>] [--json]",
|
|
146
146
|
" cockpit mail [accounts|add-imap --address <a>|inbox|read <thread>|search \"<words>\"|send --account <id> --to <a> --subject <s>|draft --account <id> --to <a> --subject <s> [--reply-to <message id>]|attachment <id> --out <path>|sync <account>|detach <account>] [--account <id>] [--limit <n>] [--unread] [--label <l>] [--file <path>] [--attach <path>]... [--dashboard-url <url>] [--json]",
|
|
147
|
-
" cockpit cal [event <id> [--occurrence-start <iso>]|export <id> [--occurrence-start <iso>]|today|week|conflicts|calendar-change <id> --color <hex-or-provider>|next|find \"<words>\"|calendars|add-ical|create --calendar <id> --title <t> --at <iso> --until <iso>|share <id> --org-visible|--private|sync <id> [--full]|detach <id>] [--tz <zone>] [--offset <n>] [--hours <n>] [--from <d> --to <d>] [--calendar <id>] [--limit <n>] [--all] [--dashboard-url <url>] [--json]",
|
|
147
|
+
" cockpit cal [event <id> [--occurrence-start <iso>]|export <id> [--occurrence-start <iso>]|today|week|conflicts|calendar-change <id> --color <hex-or-provider>|next|find \"<words>\"|calendars|add-ical|create --calendar <id> --title <t> --at <iso> --until <iso>|duplicate <event id> [--keep-invitees]|batch|undo [<change id>]|share <id> --org-visible|--private|sync <id> [--full]|detach <id>] [--tz <zone>] [--offset <n>] [--hours <n>] [--from <d> --to <d>] [--calendar <id>] [--limit <n>] [--all] [--dashboard-url <url>] [--json]",
|
|
148
148
|
" cockpit project [list] [--archived] [--dashboard-url <url>] [--json]",
|
|
149
149
|
" cockpit github <pr|merge|retitle|rerun> <owner/repo> <n> [--claim <state>] [--method squash|merge|rebase] [--title \"<text>\"] [--run <id>] [--confirm <code>] [--dashboard-url <url>] [--json]",
|
|
150
150
|
` cockpit search "<words>" [--kind <category,...>] [--limit <n>] [--dashboard-url <url>] [--json]`,
|
|
@@ -168,7 +168,7 @@ const COMMAND_SUMMARY_LINES = [
|
|
|
168
168
|
" cockpit drills [list|start|answer --confidence <1-5> [--text <words>] [--attempt <id>]|theme|theme set <theme> [--play <t>] [--watch <t>] [--train <t>]|review] [--dashboard-url <url>] [--json]",
|
|
169
169
|
" cockpit manager <clients|client <client>|run <run-id>|list <client>|queue <client>|show <id>|add <client> --title <t>|edit <id> --version <v>|done|reopen|link|approve|dismiss|proposal|archive|unarchive> [--timeline] [--dashboard-url <url>] [--json]",
|
|
170
170
|
" cockpit image <describe <file> [--session <id>]|show <sha256-or-prefix>> [--dashboard-url <url>] [--json]",
|
|
171
|
-
" cockpit setup [--since <
|
|
171
|
+
" cockpit setup [--since <duration|date>] [--until <date>] [--machines] [--details] [--person <name|email>] [--compare <name,name>] [--json]",
|
|
172
172
|
" cockpit usage sessions [--person <exact-name>] [--task <BLI-NNNN> | --subject <name>] [--repo <label>] [--tool claude|codex] [--since <window>] [--until <iso>] [--cursor <cursor>] [--limit <n>] [--json]",
|
|
173
173
|
" cockpit usage session <id> [--from-window <n>] [--json]",
|
|
174
174
|
" --by-topic groups work types; --by-subject groups open subjects from session summaries.",
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The read line, from a terminal (BLI-21299), split out of `msg.ts` to keep it
|
|
3
|
+
* under the file-size ratchet: the two fields every channel read now carries
|
|
4
|
+
* (`provenance`, `readBoundary`) and the `read --mark-read` step. The web page,
|
|
5
|
+
* MCP `msg_read` `mark_read` and this all call `POST /api/msg/channels/[id]/read`.
|
|
6
|
+
*/
|
|
7
|
+
import { askAgentDoor } from "./agent-door.js";
|
|
8
|
+
import { writeLine } from "./cli-io.js";
|
|
9
|
+
const TAG = "[msg cli]";
|
|
10
|
+
const WRITE_DEADLINE_MS = 30_000;
|
|
11
|
+
/** POST the read line to the newest top-level message of a page just read (newest first). */
|
|
12
|
+
export async function markReadThrough(door, channelId, messages) {
|
|
13
|
+
const newest = messages.find((message) => !message.thread_parent_id);
|
|
14
|
+
if (!newest) {
|
|
15
|
+
writeLine(door.io.stderr, `${TAG} mark-read skipped ${JSON.stringify({ channel_id: channelId, reason: "nothing_to_mark" })}`);
|
|
16
|
+
return { ok: false, reason: "nothing_to_mark", detail: "There was no top-level message on this page to mark read." };
|
|
17
|
+
}
|
|
18
|
+
const answer = await askAgentDoor(door, {
|
|
19
|
+
path: `/api/msg/channels/${encodeURIComponent(channelId)}/read`,
|
|
20
|
+
method: "POST",
|
|
21
|
+
label: "msg mark-read",
|
|
22
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
23
|
+
body: { message_id: newest.id },
|
|
24
|
+
});
|
|
25
|
+
if (!answer.ok) {
|
|
26
|
+
writeLine(door.io.stderr, `${TAG} mark-read failed ${JSON.stringify({ channel_id: channelId, reason: answer.reason })}`);
|
|
27
|
+
return { ok: false, reason: answer.reason, detail: answer.detail };
|
|
28
|
+
}
|
|
29
|
+
const changed = answer.body.move?.changed === true;
|
|
30
|
+
writeLine(door.io.stderr, `${TAG} mark-read ${JSON.stringify({ channel_id: channelId, message_id: newest.id, changed })}`);
|
|
31
|
+
return { ok: true, messageId: newest.id, changed };
|
|
32
|
+
}
|