@bli-cockpit/cli 0.2.239 → 0.2.241
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-view-settings.js +61 -0
- package/dist/commands/cal.js +11 -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-arg-values.js +90 -19
- package/dist/commands/local-args-tower-cal.js +148 -8
- 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-manager.js +128 -28
- 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 +35 -21
- package/dist/commands/local-help-commands-tower.js +15 -11
- package/dist/commands/local-help.js +1 -1
- package/dist/commands/manager-fields-file.js +208 -0
- package/dist/commands/manager-hints.js +88 -0
- package/dist/commands/manager-limits.json +269 -0
- package/dist/commands/manager-read-reasons.js +78 -0
- package/dist/commands/manager-render-home.js +213 -37
- package/dist/commands/manager-render.js +119 -21
- package/dist/commands/manager-requests.js +7 -3
- package/dist/commands/manager.js +67 -43
- 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
|
@@ -4,27 +4,33 @@
|
|
|
4
4
|
* cockpit manager clients
|
|
5
5
|
* cockpit manager client <client>
|
|
6
6
|
* cockpit manager home <client>
|
|
7
|
-
* cockpit manager summary <client> --version <v>|none [--health on_track|at_risk|blocked] [--headline <t>] [--bullet
|
|
7
|
+
* cockpit manager summary <client> --version <v>|none [--health on_track|at_risk|blocked] [--headline <t>] [--bullet 'YYYY-MM-DD: t'|'t']... [--next <t>|none] [--blocker <t>|none]
|
|
8
8
|
* cockpit manager run <run-id>
|
|
9
9
|
* cockpit manager list <client> [--timeline] [--needs-review] [--status open|done|dropped|closed|all] [--owner <name> | --no-owner] [--overdue] [--kind <kind>] [--search <text>] [--archived] [--review dismissed|all]
|
|
10
10
|
* cockpit manager queue <client>
|
|
11
11
|
* cockpit manager show <id> [--timeline]
|
|
12
|
-
* cockpit manager add <client> --title <t> [--owners
|
|
12
|
+
* cockpit manager add <client> --title <t> [--owners 'A, B'] [--due YYYY-MM-DD] [--meeting <timeline-id>] [--detail <text>] [--source-url <url>] [--source-note <text>] [--request-id <uuid>]
|
|
13
13
|
* cockpit manager add <client> --timeline --kind <kind> --title <t> [--date YYYY-MM-DD] [--owner <one name>] [--detail <text>] [--source-url <url>] [--source-note <text>] [--request-id <uuid>]
|
|
14
|
-
* cockpit manager edit <id> --version <v> [--timeline] [--title] [--detail] [--due <date>|none] [--owners
|
|
14
|
+
* cockpit manager edit <id> --version <v> [--timeline] [--title] [--detail] [--due <date>|none] [--owners 'A, B'|none]
|
|
15
15
|
* (timeline: --kind, --date <date>|none, --owner <one name>|none)
|
|
16
|
-
* cockpit manager done|reopen <action> --version <v>
|
|
16
|
+
* cockpit manager done|drop|reopen <action> --version <v>
|
|
17
17
|
* cockpit manager link <action> --version <v> --meeting <timeline-id> | --none
|
|
18
18
|
* cockpit manager approve <id> --version <v> [--timeline] [--restore] (--restore brings a dismissed item back)
|
|
19
19
|
* cockpit manager dismiss|archive|unarchive <id> --version <v> [--timeline]
|
|
20
20
|
* cockpit manager proposal <id> <proposal-id> accept|dismiss|add-new --version <v> [--timeline]
|
|
21
21
|
*
|
|
22
|
+
* Free text goes in a JSON file or on stdin with `--fields-file <path>|-` on add, edit and summary (no shell quoting at all,
|
|
23
|
+
* `manager-fields-file.ts`); on a flag it goes in SINGLE quotes (the shell leaves `$` alone) and the help says how to write
|
|
24
|
+
* an apostrophe. The parser is synchronous and a file is not: `--fields-file` parses to a command that carries `fieldsFile`
|
|
25
|
+
* and `rawArgs`, and `runManager` reads the file and parses the same args again with its text (`options.fieldsText`).
|
|
22
26
|
* Every verb is one `/api/manager` door on this machine's device token. A bare `cockpit manager`,
|
|
23
27
|
* `help` and `--help` are the help, never a list and never another command.
|
|
24
28
|
*/
|
|
25
|
-
import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
29
|
+
import { optionalNonEmpty, optionalUrl, parseNamedArgs, rejectSecretLikeValue } from "./local-arg-values.js";
|
|
30
|
+
import { mergeFieldsFile } from "./manager-fields-file.js";
|
|
31
|
+
import { runIdRefusal } from "./manager-hints.js";
|
|
26
32
|
export const MANAGER_VERBS = [
|
|
27
|
-
"clients", "client", "home", "summary", "run", "list", "queue", "show", "add", "edit", "done", "reopen", "link",
|
|
33
|
+
"clients", "client", "home", "summary", "run", "list", "queue", "show", "add", "edit", "done", "drop", "reopen", "link",
|
|
28
34
|
"approve", "dismiss", "proposal", "archive", "unarchive",
|
|
29
35
|
];
|
|
30
36
|
export const MANAGER_ACTION_STATUSES = ["open", "done", "dropped", "closed", "all"];
|
|
@@ -38,14 +44,16 @@ const ONE_VERB = `manager takes one verb: ${MANAGER_VERBS.join(", ")}. Example:
|
|
|
38
44
|
const OWN_FLAGS = [
|
|
39
45
|
"--timeline", "--needs-review", "--status", "--owner", "--owners", "--no-owner", "--kind", "--search", "--archived", "--review",
|
|
40
46
|
"--title", "--detail", "--due", "--date", "--meeting", "--none", "--version", "--request-id", "--restore", "--overdue", "--source-url", "--source-note",
|
|
41
|
-
"--health", "--headline", "--bullet", "--next", "--blocker",
|
|
47
|
+
"--health", "--headline", "--bullet", "--next", "--blocker", "--fields-file",
|
|
42
48
|
];
|
|
43
49
|
const COMMON_FLAGS = ["--home", "--dashboard-url", "--json", "--help"];
|
|
44
50
|
const VALUE_FLAGS = [
|
|
45
51
|
"--status", "--owner", "--owners", "--kind", "--search", "--review", "--title", "--detail", "--due", "--date", "--meeting",
|
|
46
52
|
"--version", "--request-id", "--source-url", "--source-note", "--home", "--dashboard-url",
|
|
47
|
-
"--health", "--headline", "--bullet", "--next", "--blocker",
|
|
53
|
+
"--health", "--headline", "--bullet", "--next", "--blocker", "--fields-file",
|
|
48
54
|
];
|
|
55
|
+
/** The flags whose value is a person's own words (not an id, a version or a choice). */
|
|
56
|
+
const TEXT_FLAGS = ["--title", "--detail", "--headline", "--bullet", "--next", "--blocker", "--owners", "--owner", "--source-note", "--source-url"];
|
|
49
57
|
/** `summary --bullet` is given once per bullet: the one value flag that may repeat. */
|
|
50
58
|
const REPEATABLE_FLAGS = new Set(["--bullet"]);
|
|
51
59
|
/** Which flags each verb owns, so a flag on the wrong verb is refused by name rather than ignored. */
|
|
@@ -53,14 +61,15 @@ export const FLAGS_BY_VERB = {
|
|
|
53
61
|
clients: [],
|
|
54
62
|
client: [],
|
|
55
63
|
home: [],
|
|
56
|
-
summary: ["--version", "--health", "--headline", "--bullet", "--next", "--blocker"],
|
|
64
|
+
summary: ["--version", "--health", "--headline", "--bullet", "--next", "--blocker", "--fields-file"],
|
|
57
65
|
run: [],
|
|
58
66
|
list: ["--timeline", "--needs-review", "--status", "--owner", "--no-owner", "--overdue", "--kind", "--search", "--archived", "--review"],
|
|
59
67
|
queue: [],
|
|
60
68
|
show: ["--timeline"],
|
|
61
|
-
add: ["--timeline", "--title", "--detail", "--owners", "--owner", "--due", "--meeting", "--kind", "--date", "--source-url", "--source-note", "--request-id"],
|
|
62
|
-
edit: ["--timeline", "--version", "--title", "--detail", "--owners", "--owner", "--due", "--kind", "--date"],
|
|
69
|
+
add: ["--timeline", "--title", "--detail", "--owners", "--owner", "--due", "--meeting", "--kind", "--date", "--source-url", "--source-note", "--request-id", "--fields-file"],
|
|
70
|
+
edit: ["--timeline", "--version", "--title", "--detail", "--owners", "--owner", "--due", "--kind", "--date", "--fields-file"],
|
|
63
71
|
done: ["--version"],
|
|
72
|
+
drop: ["--version"],
|
|
64
73
|
reopen: ["--version"],
|
|
65
74
|
link: ["--version", "--meeting", "--none"],
|
|
66
75
|
approve: ["--timeline", "--version", "--restore"],
|
|
@@ -82,6 +91,7 @@ const POSITIONALS = {
|
|
|
82
91
|
add: ["client"],
|
|
83
92
|
edit: ["id"],
|
|
84
93
|
done: ["action id"],
|
|
94
|
+
drop: ["action id"],
|
|
85
95
|
reopen: ["action id"],
|
|
86
96
|
link: ["action id"],
|
|
87
97
|
approve: ["id"],
|
|
@@ -103,9 +113,13 @@ function refuseRepeatedFlags(args) {
|
|
|
103
113
|
}
|
|
104
114
|
}
|
|
105
115
|
}
|
|
106
|
-
export function parseManagerArgs(args) {
|
|
116
|
+
export function parseManagerArgs(args, options = {}) {
|
|
107
117
|
refuseRepeatedFlags(args);
|
|
108
118
|
const values = parseNamedArgs(args, { allowedFlags: [...OWN_FLAGS, ...COMMON_FLAGS], valueFlags: VALUE_FLAGS });
|
|
119
|
+
// Free text is the one place a key could be pasted by mistake: the wider secret rulebook runs on these flags (and on a fields file's values), no other command's.
|
|
120
|
+
for (const flag of TEXT_FLAGS)
|
|
121
|
+
for (const value of values.flagValues.get(flag) ?? [])
|
|
122
|
+
rejectSecretLikeValue(value, flag);
|
|
109
123
|
const base = {
|
|
110
124
|
kind: "manager",
|
|
111
125
|
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
@@ -136,9 +150,9 @@ export function parseManagerArgs(args) {
|
|
|
136
150
|
if (CLIENT_VERBS.has(verb))
|
|
137
151
|
shape.client = rest[0];
|
|
138
152
|
else if (verb === "run")
|
|
139
|
-
shape.runId =
|
|
153
|
+
shape.runId = readRunId(rest[0]);
|
|
140
154
|
else if (verb !== "clients")
|
|
141
|
-
shape.id = readId(rest[0], verb === "done" || verb === "reopen" || verb === "link" ? "An action" : "An item");
|
|
155
|
+
shape.id = readId(rest[0], verb === "done" || verb === "drop" || verb === "reopen" || verb === "link" ? "An action" : "An item");
|
|
142
156
|
if (verb === "proposal") {
|
|
143
157
|
shape.proposalId = rest[1];
|
|
144
158
|
shape.decision = readDecision(rest[2]);
|
|
@@ -146,8 +160,16 @@ export function parseManagerArgs(args) {
|
|
|
146
160
|
const needsVersion = !["clients", "client", "home", "summary", "run", "list", "queue", "show", "add"].includes(verb);
|
|
147
161
|
if (needsVersion)
|
|
148
162
|
shape.version = readVersion(flag("--version"), verb);
|
|
163
|
+
let fileBullets;
|
|
164
|
+
const fieldsFile = flag("--fields-file");
|
|
165
|
+
if (fieldsFile !== undefined && (verb === "add" || verb === "edit" || verb === "summary")) {
|
|
166
|
+
// Not read yet: `runManager` reads the file (or stdin) and parses these same args again with its text.
|
|
167
|
+
if (options.fieldsText === undefined)
|
|
168
|
+
return { ...shape, fieldsFile, rawArgs: args };
|
|
169
|
+
fileBullets = mergeFieldsFile(values, verb, options.fieldsText).bullets;
|
|
170
|
+
}
|
|
149
171
|
if (verb === "summary")
|
|
150
|
-
readSummaryFields(values, shape);
|
|
172
|
+
readSummaryFields(values, shape, fileBullets);
|
|
151
173
|
if (verb === "list")
|
|
152
174
|
readListFilters(values, shape);
|
|
153
175
|
if (verb === "add")
|
|
@@ -160,6 +182,12 @@ export function parseManagerArgs(args) {
|
|
|
160
182
|
shape.restore = true;
|
|
161
183
|
return shape;
|
|
162
184
|
}
|
|
185
|
+
/** A run's receipt id: a uuid. Anything else is refused with where run ids really come from (a read's workflow id, `sched_wf_…`, is named for what it is). */
|
|
186
|
+
function readRunId(value) {
|
|
187
|
+
if (!ID.test(value))
|
|
188
|
+
throw new Error(runIdRefusal(value));
|
|
189
|
+
return value;
|
|
190
|
+
}
|
|
163
191
|
function readId(value, what) {
|
|
164
192
|
if (!ID.test(value))
|
|
165
193
|
throw new Error(`${what} id looks like 8f14e45f-ceea-467a-9575-1d3a4e7b2c10 (\`cockpit manager list <client>\` prints them).`);
|
|
@@ -253,13 +281,24 @@ function refuseWrongItemFlags(values, shape) {
|
|
|
253
281
|
}
|
|
254
282
|
else {
|
|
255
283
|
if (given("--owner"))
|
|
256
|
-
throw new Error("An action has several owners: use --owners
|
|
284
|
+
throw new Error("An action has several owners: use --owners 'A, B' (or --owners none). --owner is for a timeline item.");
|
|
257
285
|
const item = ["--kind", "--date"].find(given);
|
|
258
286
|
if (item)
|
|
259
287
|
throw new Error(`${item} belongs to a timeline item. Add --timeline, or use --due for an action's date.`);
|
|
260
288
|
}
|
|
261
289
|
}
|
|
290
|
+
/** A day written with a month name at the start of a bullet: "Oct 3:", "Sat, Oct 3rd, 2026:", "3 October:". */
|
|
291
|
+
const MONTH = "(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|june?|july?|aug(?:ust)?|sep(?:t(?:ember)?)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\\.?";
|
|
292
|
+
const MONTH_NAME_DAY = new RegExp(`^((?:(?:mon|tue|wed|thu|fri|sat|sun)[a-z]*\\.?,?\\s+)?(?:${MONTH}\\s+\\d{1,2}(?:st|nd|rd|th)?|\\d{1,2}(?:st|nd|rd|th)?\\s+${MONTH})(?:,?\\s+\\d{4})?)\\s*:`, "i");
|
|
262
293
|
const isNone = (value) => value.trim().toLowerCase() === "none";
|
|
294
|
+
/**
|
|
295
|
+
* Nothing a person can read: empty, whitespace, or only invisible characters, the same set the server's "Where we are"
|
|
296
|
+
* blank test reads as invisible (`summary-answer.ts` INVISIBLE: default-ignorable code points, format characters,
|
|
297
|
+
* combining marks such as U+FE0F and U+034F, the Hangul fillers and the blank braille U+2800), plus control characters.
|
|
298
|
+
* `trim()` alone leaves U+FE0F and U+200B behind, so free text that looks empty used to be sent (and, as --next, cleared
|
|
299
|
+
* the field while the terminal said "Saved").
|
|
300
|
+
*/
|
|
301
|
+
const isBlank = (value) => /^[\s\p{Z}\p{Cc}\p{Default_Ignorable_Code_Point}\p{Cf}\p{M}\u115F\u1160\u3164\uFFA0\u2800]*$/u.test(value);
|
|
263
302
|
/** `--owners "A, B"` replaces the whole list; `none` is the only way to clear it; a list of blanks is neither. */
|
|
264
303
|
function readOwnersList(raw) {
|
|
265
304
|
if (isNone(raw))
|
|
@@ -282,7 +321,11 @@ function readOneOwner(raw) {
|
|
|
282
321
|
}
|
|
283
322
|
function readAddFields(values, shape) {
|
|
284
323
|
refuseWrongItemFlags(values, shape);
|
|
285
|
-
const
|
|
324
|
+
const raw = values.flags.get("--title");
|
|
325
|
+
// A title of only invisible characters (U+200B, U+FE0F, U+3164 …) shows nothing: refused as blank, like edit's.
|
|
326
|
+
if (raw !== undefined && raw.trim() !== "" && isBlank(raw))
|
|
327
|
+
throw new Error("--title cannot be blank.");
|
|
328
|
+
const title = optionalNonEmpty(raw);
|
|
286
329
|
if (!title)
|
|
287
330
|
throw new Error("`cockpit manager add` needs --title <text>.");
|
|
288
331
|
shape.title = title;
|
|
@@ -338,7 +381,7 @@ function readEditFields(values, shape) {
|
|
|
338
381
|
let changes = 0;
|
|
339
382
|
const title = values.flags.get("--title");
|
|
340
383
|
if (title !== undefined) {
|
|
341
|
-
if (title
|
|
384
|
+
if (isBlank(title))
|
|
342
385
|
throw new Error("--title cannot be blank.");
|
|
343
386
|
shape.title = title.trim();
|
|
344
387
|
changes += 1;
|
|
@@ -397,24 +440,69 @@ function readSummaryVersion(raw) {
|
|
|
397
440
|
throw new Error("--version is the card's summary.updatedAt exactly as `cockpit manager home <client> --json` gives it, like 2026-10-09T17:03:11.123456+00:00, or none.");
|
|
398
441
|
return raw;
|
|
399
442
|
}
|
|
400
|
-
/**
|
|
443
|
+
/**
|
|
444
|
+
* The card's text limits, in characters (code points), as the help and the server's `TOWER_MANAGER_SUMMARY_LIMITS` say
|
|
445
|
+
* (`manager-cli-limits.test.ts` in the dashboard fails when they differ). Checked here so a long text is refused by this
|
|
446
|
+
* sentence, not by the server's raw schema message ("String must contain at most 4000 character(s)") that a text over 4,000 reaches first.
|
|
447
|
+
*/
|
|
448
|
+
export const SUMMARY_TEXT_LIMITS = { headline: 140, bullet: 160, next: 200, blocker: 200 };
|
|
449
|
+
function refuseTooLong(flag, text, max) {
|
|
450
|
+
const length = [...text].length;
|
|
451
|
+
if (length > max)
|
|
452
|
+
throw new Error(`${flag} is ${length.toLocaleString("en-US")} characters; at most ${max}. Shorten it.`);
|
|
453
|
+
}
|
|
454
|
+
/** `--bullet '2026-10-14: Signed the SOW'` is a dated bullet; without a leading `YYYY-MM-DD: ` it is undated. */
|
|
401
455
|
export function readBullet(raw) {
|
|
402
456
|
// A bullet pasted from a list keeps no "- " or "* " of its own.
|
|
403
457
|
const value = raw.trim().replace(/^[-*•]\s+/u, "");
|
|
404
|
-
const dated = /^(\d{4}-\d{2}-\d{2})
|
|
405
|
-
|
|
458
|
+
const dated = /^(\d{4}-\d{2}-\d{2})\s*:\s*(.*)$/s.exec(value);
|
|
459
|
+
// A shape of digits split by dashes or slashes was always read as a mistyped day ("26-10-01:", "10/14/2026:",
|
|
460
|
+
// "2026-10/14:"). A DOT only counts with a 4-digit year: first and then dash/slash or dot ("2026-10.14:",
|
|
461
|
+
// "2026.10.14:"), or last in a dotted-only shape ("14.10.2026:"). Short ranges ("1-2.5:", "9-10.30:") and
|
|
462
|
+
// versions ("2.1.0:") are plain text.
|
|
463
|
+
const dateLike = dated ? null : /^(\d{1,4}[-/]\d{1,2}[-/]\d{1,4}|\d{4}[-/]\d{1,2}\.\d{1,4}|\d{4}\.\d{1,2}\.\d{1,2}|\d{1,2}\.\d{1,2}\.\d{4})\s*:/.exec(value);
|
|
406
464
|
if (dateLike)
|
|
407
465
|
throw new Error(`--bullet "${raw}" starts with ${dateLike[1]}, which is not a YYYY-MM-DD day. Write it like 2026-10-14, or leave the date out for an undated bullet.`);
|
|
466
|
+
// A day spelled with a month name ("Oct 3: x", "3 October 2026: x") is a date too: saved undated it would lose the day without a word.
|
|
467
|
+
const worded = dated ? null : MONTH_NAME_DAY.exec(value);
|
|
468
|
+
if (worded)
|
|
469
|
+
throw new Error(`--bullet "${raw}" starts with ${worded[1].trim()}, which looks like a date but is not YYYY-MM-DD. Write it like 2026-10-14 to date the bullet, or reword the text so it does not start with a date.`);
|
|
408
470
|
const date = dated ? dated[1] : null;
|
|
409
471
|
const text = (dated ? dated[2] : value).trim();
|
|
410
472
|
if (date !== null && !isCalendarDate(date))
|
|
411
473
|
throw new Error(`--bullet "${raw}" starts with ${date}, which is not a real day. Write the date YYYY-MM-DD, like 2026-10-14, or leave it out for an undated bullet.`);
|
|
412
|
-
|
|
413
|
-
|
|
474
|
+
return checkedBullet(date, text);
|
|
475
|
+
}
|
|
476
|
+
/** A bullet's words are never blank (the same rule for `--bullet` and a fields file's `{date, text}`). */
|
|
477
|
+
function checkedBullet(date, text) {
|
|
478
|
+
if (isBlank(text)) {
|
|
479
|
+
throw new Error(date === null
|
|
480
|
+
? "--bullet needs some words, and this one is empty. Example: --bullet 'Export still missing' (a day first makes it dated: --bullet '2026-10-14: Signed the SOW')."
|
|
481
|
+
: "--bullet needs words after its date. Example: --bullet '2026-10-14: Signed the SOW'.");
|
|
482
|
+
}
|
|
414
483
|
return { date, text };
|
|
415
484
|
}
|
|
485
|
+
/**
|
|
486
|
+
* A fields file's `{date, text}` bullet. With a date, the date must be a real day and the text is the text. With none (null or
|
|
487
|
+
* left out), the text is read exactly like a `--bullet` value: a leading "YYYY-MM-DD: " dates it, a day spelled any other way is
|
|
488
|
+
* refused, never saved undated (the same rule MCP's manager_set_summary applies).
|
|
489
|
+
*/
|
|
490
|
+
function readObjectBullet(bullet) {
|
|
491
|
+
if (bullet.date === null)
|
|
492
|
+
return readBullet(bullet.text);
|
|
493
|
+
if (!isCalendarDate(bullet.date)) {
|
|
494
|
+
throw new Error(`A bullet's date "${bullet.date}" is not a real day. Write the date YYYY-MM-DD, like 2026-10-14, or null for an undated bullet.`);
|
|
495
|
+
}
|
|
496
|
+
return checkedBullet(bullet.date, bullet.text.trim());
|
|
497
|
+
}
|
|
498
|
+
/** --next and --blocker: text, or `none` to clear. A value with nothing readable in it is refused, never sent as a silent clear. */
|
|
499
|
+
function readClearable(flag, raw) {
|
|
500
|
+
if (isBlank(raw))
|
|
501
|
+
throw new Error(`${flag} is blank, and a blank would clear it. To clear ${flag} write ${flag} none; otherwise give words.`);
|
|
502
|
+
return raw.trim();
|
|
503
|
+
}
|
|
416
504
|
/** The card's fields. Counts, lengths and "blocked needs a blocker" are the server's rules, refused there by name. */
|
|
417
|
-
function readSummaryFields(values, shape) {
|
|
505
|
+
function readSummaryFields(values, shape, fileBullets) {
|
|
418
506
|
shape.version = readSummaryVersion(values.flags.get("--version"));
|
|
419
507
|
let changes = 0;
|
|
420
508
|
const health = values.flags.get("--health");
|
|
@@ -426,26 +514,38 @@ function readSummaryFields(values, shape) {
|
|
|
426
514
|
}
|
|
427
515
|
const headline = values.flags.get("--headline");
|
|
428
516
|
if (headline !== undefined) {
|
|
429
|
-
if (headline
|
|
517
|
+
if (isBlank(headline))
|
|
430
518
|
throw new Error("--headline cannot be blank.");
|
|
519
|
+
refuseTooLong("--headline", headline.trim(), SUMMARY_TEXT_LIMITS.headline);
|
|
431
520
|
shape.headline = headline.trim();
|
|
432
521
|
changes += 1;
|
|
433
522
|
}
|
|
434
523
|
const bullets = values.flagValues.get("--bullet");
|
|
435
|
-
if (
|
|
524
|
+
if (fileBullets !== undefined) {
|
|
525
|
+
// An empty list is a mistake, not "no change": the server wants 1 to 5 and refuses 0 by name.
|
|
526
|
+
shape.bullets = fileBullets.map((bullet) => (typeof bullet === "string" ? readBullet(bullet) : readObjectBullet(bullet)));
|
|
527
|
+
changes += 1;
|
|
528
|
+
}
|
|
529
|
+
else if (bullets !== undefined && bullets.length > 0) {
|
|
436
530
|
shape.bullets = bullets.map(readBullet);
|
|
437
531
|
changes += 1;
|
|
438
532
|
}
|
|
533
|
+
for (const bullet of shape.bullets ?? [])
|
|
534
|
+
refuseTooLong("A bullet", bullet.text, SUMMARY_TEXT_LIMITS.bullet);
|
|
439
535
|
const next = values.flags.get("--next");
|
|
440
536
|
if (next !== undefined) {
|
|
441
|
-
shape.next = isNone(next) ? null : next
|
|
537
|
+
shape.next = isNone(next) ? null : readClearable("--next", next);
|
|
442
538
|
changes += 1;
|
|
443
539
|
}
|
|
444
540
|
const blocker = values.flags.get("--blocker");
|
|
445
541
|
if (blocker !== undefined) {
|
|
446
|
-
shape.blocker = isNone(blocker) ? null : blocker
|
|
542
|
+
shape.blocker = isNone(blocker) ? null : readClearable("--blocker", blocker);
|
|
447
543
|
changes += 1;
|
|
448
544
|
}
|
|
545
|
+
if (shape.next)
|
|
546
|
+
refuseTooLong("--next", shape.next, SUMMARY_TEXT_LIMITS.next);
|
|
547
|
+
if (shape.blocker)
|
|
548
|
+
refuseTooLong("--blocker", shape.blocker, SUMMARY_TEXT_LIMITS.blocker);
|
|
449
549
|
if (changes === 0) {
|
|
450
550
|
throw new Error("`cockpit manager summary` has nothing to change. Give at least one of --health, --headline, --bullet (1 to 5 of them, replacing the list), --next, --blocker. A flag you leave out keeps its value; none clears --next or --blocker.");
|
|
451
551
|
}
|
|
@@ -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
|
}
|