@bli-cockpit/cli 0.2.242 → 0.2.244

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.
Files changed (135) hide show
  1. package/dist/adapters/git-state.js +1 -0
  2. package/dist/adapters/raw-evidence-budget-admission.js +63 -0
  3. package/dist/adapters/raw-evidence-collection-state.js +6 -50
  4. package/dist/adapters/raw-evidence-git-diff.js +1 -0
  5. package/dist/agent-rules.js +8 -0
  6. package/dist/autostart-contract.js +12 -0
  7. package/dist/autostart-darwin-plist.js +2 -2
  8. package/dist/autostart-darwin.js +1 -1
  9. package/dist/autostart-remind.js +245 -0
  10. package/dist/autostart-self-heal.js +17 -1
  11. package/dist/autostart-windows-scripts.js +7 -7
  12. package/dist/autostart.js +16 -15
  13. package/dist/commands/approvals-check-where.js +14 -0
  14. package/dist/commands/approvals-render.js +8 -1
  15. package/dist/commands/approvals.js +3 -2
  16. package/dist/commands/autostart-command.js +10 -0
  17. package/dist/commands/autostart-heal.js +36 -1
  18. package/dist/commands/brain.js +1 -1
  19. package/dist/commands/cal-agenda.js +44 -0
  20. package/dist/commands/cal-availability-share.js +97 -0
  21. package/dist/commands/cal-booking.js +147 -0
  22. package/dist/commands/cal-join.js +62 -0
  23. package/dist/commands/cal-links.js +68 -0
  24. package/dist/commands/cal-people.js +136 -0
  25. package/dist/commands/cal-remind.js +176 -0
  26. package/dist/commands/cal-reminders.js +33 -0
  27. package/dist/commands/cal-suggest.js +89 -0
  28. package/dist/commands/cal.js +41 -1
  29. package/dist/commands/cycle-args.js +127 -0
  30. package/dist/commands/cycles.js +117 -0
  31. package/dist/commands/docs-comments-args.js +85 -0
  32. package/dist/commands/docs-comments.js +128 -0
  33. package/dist/commands/docs-files-args.js +49 -0
  34. package/dist/commands/docs-files.js +179 -0
  35. package/dist/commands/docs-formatting.js +45 -0
  36. package/dist/commands/docs-inbox-args.js +93 -0
  37. package/dist/commands/docs-inbox.js +126 -0
  38. package/dist/commands/docs-notion.js +5 -0
  39. package/dist/commands/docs-read.js +2 -1
  40. package/dist/commands/docs-recovery.js +173 -0
  41. package/dist/commands/docs-refs-args.js +104 -0
  42. package/dist/commands/docs-refs.js +153 -0
  43. package/dist/commands/docs-search-args.js +79 -0
  44. package/dist/commands/docs-search.js +64 -0
  45. package/dist/commands/docs-sync-args.js +82 -0
  46. package/dist/commands/docs-sync.js +197 -0
  47. package/dist/commands/docs-tree.js +136 -0
  48. package/dist/commands/docs.js +85 -51
  49. package/dist/commands/doctor-update.js +4 -1
  50. package/dist/commands/inbox-args.js +81 -0
  51. package/dist/commands/inbox.js +88 -0
  52. package/dist/commands/issue-batch.js +61 -0
  53. package/dist/commands/issue-relations.js +151 -0
  54. package/dist/commands/issue-write.js +38 -5
  55. package/dist/commands/issue.js +32 -1
  56. package/dist/commands/local-args-collector-sources.js +2 -1
  57. package/dist/commands/local-args-tower-approvals.js +16 -4
  58. package/dist/commands/local-args-tower-cal-booking.js +81 -0
  59. package/dist/commands/local-args-tower-cal-links.js +42 -0
  60. package/dist/commands/local-args-tower-cal-remind.js +74 -0
  61. package/dist/commands/local-args-tower-cal-share.js +41 -0
  62. package/dist/commands/local-args-tower-cal.js +81 -8
  63. package/dist/commands/local-args-tower-docs-msg.js +98 -9
  64. package/dist/commands/local-args-tower-msg.js +222 -10
  65. package/dist/commands/local-args-tower-search.js +32 -5
  66. package/dist/commands/local-args-tower-work.js +110 -34
  67. package/dist/commands/local-auth.js +1 -1
  68. package/dist/commands/local-help-commands-tower-ops.js +6 -4
  69. package/dist/commands/local-help-commands-tower.js +47 -31
  70. package/dist/commands/local-help-commands.js +3 -1
  71. package/dist/commands/local-help-cycles.js +9 -0
  72. package/dist/commands/local-help-docs-files.js +10 -0
  73. package/dist/commands/local-help-docs-formatting.js +7 -0
  74. package/dist/commands/local-help-docs-inbox.js +10 -0
  75. package/dist/commands/local-help-docs-recovery.js +10 -0
  76. package/dist/commands/local-help-docs-refs.js +11 -0
  77. package/dist/commands/local-help-docs-sync.js +9 -0
  78. package/dist/commands/local-help-inbox.js +9 -0
  79. package/dist/commands/local-help-project.js +21 -0
  80. package/dist/commands/local-help-templates.js +9 -0
  81. package/dist/commands/local-help-views.js +10 -0
  82. package/dist/commands/local-help-work-notify.js +6 -0
  83. package/dist/commands/local-help.js +3 -3
  84. package/dist/commands/manager-hints.js +4 -2
  85. package/dist/commands/manager-limits.json +3 -1
  86. package/dist/commands/manager-read-reasons.js +1 -0
  87. package/dist/commands/manager-render-home.js +13 -5
  88. package/dist/commands/manager-render.js +3 -0
  89. package/dist/commands/manager.js +3 -1
  90. package/dist/commands/mcp-stdio-probe.js +1 -0
  91. package/dist/commands/msg-activity.js +134 -0
  92. package/dist/commands/msg-channel-args.js +92 -0
  93. package/dist/commands/msg-channel.js +125 -0
  94. package/dist/commands/msg-context-args.js +67 -0
  95. package/dist/commands/msg-context.js +65 -0
  96. package/dist/commands/msg-dm.js +28 -0
  97. package/dist/commands/msg-file.js +185 -0
  98. package/dist/commands/msg-mark-args.js +78 -0
  99. package/dist/commands/msg-mark.js +148 -0
  100. package/dist/commands/msg-notify-args.js +100 -0
  101. package/dist/commands/msg-notify.js +122 -0
  102. package/dist/commands/msg-schedule-args.js +114 -0
  103. package/dist/commands/msg-schedule.js +226 -0
  104. package/dist/commands/msg-search-args.js +71 -0
  105. package/dist/commands/msg-search.js +48 -0
  106. package/dist/commands/msg-write.js +15 -1
  107. package/dist/commands/msg.js +62 -39
  108. package/dist/commands/project-args.js +82 -0
  109. package/dist/commands/project.js +71 -7
  110. package/dist/commands/public-root.js +1 -1
  111. package/dist/commands/search.js +23 -0
  112. package/dist/commands/session-sync-failures.js +17 -4
  113. package/dist/commands/session-sync-health.js +7 -2
  114. package/dist/commands/session-sync-hold.js +42 -1
  115. package/dist/commands/session-sync-outside-roots.js +72 -14
  116. package/dist/commands/session-sync-upload.js +2 -0
  117. package/dist/commands/session-sync.js +1 -0
  118. package/dist/commands/sources-check.js +63 -2
  119. package/dist/commands/sources-discovery.js +1 -1
  120. package/dist/commands/sources-reconciliation.js +87 -0
  121. package/dist/commands/sync-doctor.js +14 -1
  122. package/dist/commands/sync-report.js +1 -1
  123. package/dist/commands/template-args.js +122 -0
  124. package/dist/commands/templates.js +162 -0
  125. package/dist/commands/view-args.js +131 -0
  126. package/dist/commands/views.js +138 -0
  127. package/dist/commands/work-notify-args.js +67 -0
  128. package/dist/commands/work-notify.js +63 -0
  129. package/dist/delivery-hold-notice.js +34 -0
  130. package/dist/evidence-transcripts-client.js +1 -0
  131. package/dist/log-rotation.js +2 -2
  132. package/dist/notifier.js +83 -0
  133. package/dist/process-runner.js +4 -0
  134. package/dist/repo-identity-git.js +1 -0
  135. package/package.json +4 -4
@@ -19,8 +19,12 @@
19
19
  * would be computed in UTC — "today" would end at 5pm in Vancouver.
20
20
  */
21
21
  import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs, } from "./local-arg-values.js";
22
+ import { BOOKING_ACTIONS, BOOKING_BOOLEANS, BOOKING_GUEST_ACTIONS, BOOKING_VALUE_FLAGS, readBookingFlags } from "./local-args-tower-cal-booking.js";
23
+ import { LINKS_ACTIONS, LINKS_BOOLEANS, LINKS_SUBJECT_ACTIONS, LINKS_VALUE_FLAGS, readLinksFlags } from "./local-args-tower-cal-links.js";
24
+ import { readRemindFlags, REMIND_ACTIONS, REMIND_BOOLEANS, REMIND_SUBJECT_ACTIONS, REMIND_VALUE_FLAGS } from "./local-args-tower-cal-remind.js";
25
+ import { readShareFlags, SHARE_ACTIONS, SHARE_BOOLEANS, SHARE_VALUE_FLAGS } from "./local-args-tower-cal-share.js";
22
26
  const CAL_ACTIONS = new Set([
23
- "event", "export", "duplicate", "batch", "undo", "conflicts", "calendar-change", "view-settings", "view-settings-change",
27
+ "event", "export", "duplicate", "batch", "undo", "conflicts", "calendar-change", "view-settings", "view-settings-change", "people", "availability", "overlays", "suggest", ...SHARE_ACTIONS, ...BOOKING_ACTIONS, ...REMIND_ACTIONS, ...LINKS_ACTIONS,
24
28
  "today",
25
29
  "week",
26
30
  "next",
@@ -36,7 +40,7 @@ const CAL_ACTIONS = new Set([
36
40
  "delete",
37
41
  ]);
38
42
  /** Every verb that names ONE thing first — a calendar id, or (delete) an event id. */
39
- const CAL_ACTIONS_NEEDING_A_SUBJECT = new Set(["event", "export", "duplicate", "calendar-change", "sync", "detach", "share", "delete", "change", "cancel", "rsvp"]);
43
+ const CAL_ACTIONS_NEEDING_A_SUBJECT = new Set([...BOOKING_GUEST_ACTIONS, ...REMIND_SUBJECT_ACTIONS, ...LINKS_SUBJECT_ACTIONS, "availability-revoke", "event", "export", "duplicate", "calendar-change", "sync", "detach", "share", "delete", "change", "cancel", "rsvp"]);
40
44
  /** `sync-status [<calendar id>]` — every calendar, or one's. Ticket 10730. */
41
45
  const CAL_ACTIONS_WITH_OPTIONAL_SUBJECT = new Set(["sync-status", "undo"]);
42
46
  const CAL_ALLOWED_FLAGS = [
@@ -73,6 +77,8 @@ const CAL_ALLOWED_FLAGS = [
73
77
  "--reason",
74
78
  "--scope", "--occurrence", "--response", "--comment", "--reminder", "--visibility", "--transparency", "--key",
75
79
  "--view", "--days", "--week-start", "--hide-weekends", "--density", "--zone",
80
+ "--with", "--pin", "--remove",
81
+ "--duration", "--weekends", "--recheck", ...SHARE_BOOLEANS, ...SHARE_VALUE_FLAGS, ...BOOKING_BOOLEANS, ...BOOKING_VALUE_FLAGS, ...REMIND_BOOLEANS, ...REMIND_VALUE_FLAGS, ...LINKS_BOOLEANS, ...LINKS_VALUE_FLAGS,
76
82
  ];
77
83
  const CAL_VALUE_FLAGS = [
78
84
  "--home",
@@ -98,6 +104,7 @@ const CAL_VALUE_FLAGS = [
98
104
  "--reason",
99
105
  "--scope", "--occurrence", "--response", "--comment", "--reminder", "--visibility", "--transparency", "--key",
100
106
  "--view", "--days", "--week-start", "--hide-weekends", "--density", "--zone",
107
+ "--with", "--pin", "--remove", "--duration", ...SHARE_VALUE_FLAGS, ...BOOKING_VALUE_FLAGS, ...REMIND_VALUE_FLAGS, ...LINKS_VALUE_FLAGS,
101
108
  ];
102
109
  export function parseCalArgs(args) {
103
110
  const values = parseNamedArgs(args, { allowedFlags: CAL_ALLOWED_FLAGS, valueFlags: CAL_VALUE_FLAGS });
@@ -111,13 +118,13 @@ export function parseCalArgs(args) {
111
118
  const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
112
119
  const offset = readCalOffset(values);
113
120
  const command = buildCalCommand(values, action, subject, query, title, startsAt, endsAt, offset, limit);
114
- return { ...command, ...readCalEventScope(action, values), ...readViewSettingsPatch(action, values) };
121
+ return { ...command, ...readCalEventScope(action, values), ...readViewSettingsPatch(action, values), ...readPeopleFlags(action, values), ...readShareFlags(action, values), ...readBookingFlags(action, values), ...readRemindFlags(action, values), ...readLinksFlags(action, values) };
115
122
  }
116
123
  function readCalAction(values) {
117
124
  const first = values.positionals[0];
118
125
  const action = (first === undefined ? "today" : first);
119
126
  if (!CAL_ACTIONS.has(action)) {
120
- throw new Error(`Unknown cal command: ${first}. Try today, week, conflicts, calendar-change, view-settings, view-settings-change, next, find, calendars, add-ical, create, duplicate, batch, undo, share, detach, sync, sync-status, or delete.`);
127
+ throw new Error(`Unknown cal command: ${first}. Try today, week, conflicts, calendar-change, view-settings, view-settings-change, people, availability, overlays, suggest, availability-share, availability-revoke, booking-page, booking-slots, book, booking-change, booking-cancel, remind, join, reminders, snooze, dismiss, links, agenda, next, find, calendars, add-ical, create, duplicate, batch, undo, share, detach, sync, sync-status, or delete.`);
121
128
  }
122
129
  return { action, rest: values.positionals.slice(first === undefined ? 0 : 1) };
123
130
  }
@@ -127,9 +134,17 @@ function readCalSubjectOrQuery(action, rest) {
127
134
  if (CAL_ACTIONS_NEEDING_A_SUBJECT.has(action)) {
128
135
  subject = optionalNonEmpty(rest[0]);
129
136
  if (!subject) {
130
- throw new Error(action === "delete" || action === "duplicate" || action === "change" || action === "cancel" || action === "rsvp"
131
- ? `cal ${action} needs an event id — take one from a window verb's output (today/week/next/find).`
132
- : `cal ${action} needs a calendar id — take one from \`cockpit cal calendars\`.`);
137
+ throw new Error(action === "availability-revoke"
138
+ ? "cal availability-revoke needs a share id — take one from `cockpit cal availability-share --list`."
139
+ : BOOKING_GUEST_ACTIONS.includes(action)
140
+ ? `cal ${action} needs the booking link (or its token) the host or Tower gave you.`
141
+ : action === "snooze" || action === "dismiss"
142
+ ? `cal ${action} needs an event id and --occurrence — take both from cal next.`
143
+ : action === "links"
144
+ ? "cal links needs an event id — take one from a window verb's output (today/week/next/find)."
145
+ : action === "delete" || action === "duplicate" || action === "change" || action === "cancel" || action === "rsvp"
146
+ ? `cal ${action} needs an event id — take one from a window verb's output (today/week/next/find).`
147
+ : `cal ${action} needs a calendar id — take one from \`cockpit cal calendars\`.`);
133
148
  }
134
149
  if (rest.length > 1)
135
150
  throw new Error(`cal ${action} takes one id, not ${rest.length}.`);
@@ -144,6 +159,11 @@ function readCalSubjectOrQuery(action, rest) {
144
159
  if (!query)
145
160
  throw new Error('cal find needs words: cockpit cal find "standup".');
146
161
  }
162
+ else if (action === "people") {
163
+ query = optionalNonEmpty(rest.join(" "));
164
+ if (!query)
165
+ throw new Error('cal people needs a name or an email: cockpit cal people "Vee".');
166
+ }
147
167
  else if (rest.length > 0) {
148
168
  throw new Error(`cal ${action} does not take "${rest[0]}".`);
149
169
  }
@@ -200,7 +220,7 @@ function buildCalCommand(values, action, subject, query, title, startsAt, endsAt
200
220
  // `commands/cal.ts` rather than here, so the parser stays pure.
201
221
  timeZone: optionalNonEmpty(values.flags.get("--tz")),
202
222
  ...(offset === undefined ? {} : { offset }),
203
- ...(optionalPositiveInteger(values.flags.get("--hours"), "--hours") === undefined
223
+ ...(action === "suggest" || optionalPositiveInteger(values.flags.get("--hours"), "--hours") === undefined
204
224
  ? {}
205
225
  : { hours: optionalPositiveInteger(values.flags.get("--hours"), "--hours") }),
206
226
  from: optionalNonEmpty(values.flags.get("--from")),
@@ -370,4 +390,57 @@ function readViewSettingsPatch(action, values) {
370
390
  patch.extra_zones = zones;
371
391
  }
372
392
  return Object.keys(patch).length ? { viewSettings: patch } : {};
393
+ }
394
+ const PEOPLE_FLAG_KEYS = ["--with", "--pin", "--remove"];
395
+ const SUGGEST_FLAG_KEYS = ["--duration", "--weekends"];
396
+ /** BLI-21522: `availability --with` (repeatable), `overlays --pin|--remove`. Each belongs to one verb. */
397
+ function readPeopleFlags(action, values) {
398
+ const owner = { "--with": "availability", "--pin": "overlays", "--remove": "overlays" };
399
+ for (const flag of PEOPLE_FLAG_KEYS) {
400
+ if (values.flags.has(flag) && owner[flag] !== action && !(flag === "--with" && action === "suggest") && !(flag === "--remove" && action === "links"))
401
+ throw new Error(`cal ${action} does not take ${flag}; it belongs to cal ${owner[flag]}.`);
402
+ }
403
+ for (const flag of SUGGEST_FLAG_KEYS) {
404
+ if ((values.flags.has(flag) || values.booleans.has(flag)) && action !== "suggest" && !(flag === "--duration" && (action === "availability-share" || action === "booking-page")))
405
+ throw new Error(`cal ${action} does not take ${flag}; it belongs to cal suggest.`);
406
+ }
407
+ if (values.booleans.has("--recheck") && action !== "create")
408
+ throw new Error(`cal ${action} does not take --recheck; it belongs to cal create.`);
409
+ if (action === "create" && values.booleans.has("--recheck"))
410
+ return { recheck: true };
411
+ if (action === "suggest")
412
+ return readSuggestFlags(values);
413
+ if (action === "availability") {
414
+ const labels = (values.flagValues.get("--with") ?? []).map((label) => label.trim()).filter(Boolean);
415
+ return labels.length ? { peopleWith: labels } : {};
416
+ }
417
+ if (action === "overlays") {
418
+ const pin = optionalNonEmpty(values.flags.get("--pin"));
419
+ const remove = optionalNonEmpty(values.flags.get("--remove"));
420
+ if (pin && remove)
421
+ throw new Error("cal overlays takes --pin or --remove, not both.");
422
+ return { ...(pin ? { pinLabel: pin } : {}), ...(remove ? { removeEmail: remove } : {}) };
423
+ }
424
+ return {};
425
+ }
426
+ /** BLI-21542 `suggest --with ... --duration 30 --hours 09:00-17:00 --weekends`: shape only; the door owns the limits. */
427
+ function readSuggestFlags(values) {
428
+ const labels = (values.flagValues.get("--with") ?? []).map((label) => label.trim()).filter(Boolean);
429
+ const out = labels.length ? { peopleWith: labels } : {};
430
+ const duration = optionalNonEmpty(values.flags.get("--duration"));
431
+ if (duration !== undefined) {
432
+ if (!/^\d+$/.test(duration))
433
+ throw new Error("--duration takes a whole number of minutes.");
434
+ out.duration = Number(duration);
435
+ }
436
+ const hours = optionalNonEmpty(values.flags.get("--hours"));
437
+ if (hours !== undefined) {
438
+ const match = /^(\d{1,2}:\d{2})-(\d{1,2}:\d{2})$/.exec(hours);
439
+ if (!match)
440
+ throw new Error("--hours takes a range like 09:00-17:00.");
441
+ out.hoursRange = { start: match[1], end: match[2] };
442
+ }
443
+ if (values.booleans.has("--weekends"))
444
+ out.weekends = true;
445
+ return out;
373
446
  }
@@ -7,15 +7,22 @@
7
7
  * added both without stretching either existing family's own doc comment.
8
8
  */
9
9
  import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs, } from "./local-arg-values.js";
10
+ import { DOC_SEARCH_BOOLEAN_FLAGS, DOC_SEARCH_VALUE_FLAGS, hasDocScopeFlags, readDocSearchFilters } from "./docs-search-args.js";
11
+ import { parseDocsCommentsArgs } from "./docs-comments-args.js";
12
+ import { parseDocsFilesArgs } from "./docs-files-args.js";
13
+ import { isDocsRefsInvocation, parseDocsRefsArgs } from "./docs-refs-args.js";
14
+ import { isDocsSyncInvocation, parseDocsSyncArgs } from "./docs-sync-args.js";
15
+ import { parseDocsFormattingArgs } from "./docs-formatting.js";
16
+ import { parseDocsInboxArgs } from "./docs-inbox-args.js";
10
17
  import { parseDocsExportFlags } from "./local-args-docs-export.js";
11
- const DOCS_ACTIONS = new Set(["list", "tree", "read", "create", "update", "delete", "link", "sync-status", "export", "mutation-status", "share", "access"]);
18
+ const DOCS_ACTIONS = new Set(["list", "tree", "read", "create", "update", "delete", "link", "sync-status", "export", "mutation-status", "share", "access", "trash", "restore", "versions", "favorite", "unfavorite", "marks", "search", "comments", "comment", "inbox", "inbox-read", "subscribe", "unsubscribe", "notifications", "files", "file"]);
12
19
  const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
13
20
  // BLI-10450: `read` moved to its own branch in `parseDocsDocumentReference` —
14
21
  // it takes SEVERAL references now, so it no longer shares the "exactly one"
15
22
  // rule `update`/`link` still enforce. BLI‑10555: `delete` takes several too
16
23
  // (the same door, called once per ref), so it joined `read`'s branch rather
17
24
  // than this set.
18
- const DOCS_ACTIONS_NEEDING_A_DOC = new Set(["update", "link", "export", "mutation-status", "share", "access"]);
25
+ const DOCS_ACTIONS_NEEDING_A_DOC = new Set(["update", "link", "export", "mutation-status", "share", "access", "restore", "versions", "favorite", "unfavorite"]);
19
26
  const DOCS_ALLOWED_FLAGS = [
20
27
  "--home",
21
28
  "--dashboard-url",
@@ -33,6 +40,7 @@ const DOCS_ALLOWED_FLAGS = [
33
40
  "--limit",
34
41
  "--clear",
35
42
  "--bodies",
43
+ "--all",
36
44
  "--section",
37
45
  "--offset",
38
46
  "--format",
@@ -40,6 +48,9 @@ const DOCS_ALLOWED_FLAGS = [
40
48
  "--add",
41
49
  "--remove",
42
50
  "--reason",
51
+ "--cursor",
52
+ "--before",
53
+ "--version",
43
54
  "--json",
44
55
  ];
45
56
  const DOCS_VALUE_FLAGS = [
@@ -60,8 +71,53 @@ const DOCS_VALUE_FLAGS = [
60
71
  "--add",
61
72
  "--remove",
62
73
  "--reason",
74
+ "--cursor",
75
+ "--before",
76
+ "--version",
63
77
  ];
78
+ /**
79
+ * BLI-21697: `docs search [words] [--word] [--title-only] [--by <me|email|id>] [--under <id|slug>] [--since D]
80
+ * [--until D] [--timezone Z] [--sort relevance|updated] [--limit n]`. Words are positionals joined with a space; a
81
+ * person, subtree or date filter makes an empty query a search.
82
+ */
83
+ function parseDocsSearchArgs(args) {
84
+ const values = parseNamedArgs(args, {
85
+ allowedFlags: ["--home", "--dashboard-url", "--limit", "--json", "--since", "--until", "--timezone", ...DOC_SEARCH_VALUE_FLAGS, ...DOC_SEARCH_BOOLEAN_FLAGS],
86
+ valueFlags: ["--home", "--dashboard-url", "--limit", "--since", "--until", "--timezone", ...DOC_SEARCH_VALUE_FLAGS],
87
+ });
88
+ const filters = readDocSearchFilters(values);
89
+ const query = values.positionals.slice(1).join(" ").trim();
90
+ if (query.length === 0 && !hasDocScopeFlags(filters)) {
91
+ throw new Error('docs search needs words to look for, e.g. `cockpit docs search "storage ceiling"`, or a filter such as --by me.');
92
+ }
93
+ return {
94
+ kind: "docs",
95
+ action: "search",
96
+ clearParent: false,
97
+ bodyStdin: false,
98
+ ...(query ? { query } : {}),
99
+ searchFilters: filters,
100
+ limit: optionalPositiveInteger(values.flags.get("--limit"), "--limit"),
101
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
102
+ dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
103
+ json: values.booleans.has("--json"),
104
+ };
105
+ }
64
106
  export function parseDocsArgs(args) {
107
+ if (args[0] === "search")
108
+ return parseDocsSearchArgs(args);
109
+ if (args[0] === "comments" || args[0] === "comment")
110
+ return parseDocsCommentsArgs(args);
111
+ if (args[0] === "files" || args[0] === "file")
112
+ return parseDocsFilesArgs(args);
113
+ if (isDocsRefsInvocation(args))
114
+ return parseDocsRefsArgs(args);
115
+ if (isDocsSyncInvocation(args))
116
+ return parseDocsSyncArgs(args);
117
+ if (args[0] === "formatting")
118
+ return parseDocsFormattingArgs(args);
119
+ if (args[0] === "inbox" || args[0] === "subscribe" || args[0] === "unsubscribe" || args[0] === "notifications")
120
+ return parseDocsInboxArgs(args);
65
121
  const values = parseNamedArgs(args, { allowedFlags: DOCS_ALLOWED_FLAGS, valueFlags: DOCS_VALUE_FLAGS });
66
122
  const { action, rest } = parseDocsAction(values.positionals);
67
123
  const link = parseDocsLink(action, rest, values.booleans.has("--clear"));
@@ -77,7 +133,7 @@ export function parseDocsArgs(args) {
77
133
  if (action === "delete" && (!docRefs || docRefs.length === 0)) {
78
134
  throw new Error("docs delete needs one or more document ids/slugs.");
79
135
  }
80
- return buildDocsCommand(values, action, docRef, docRefs, visibility, clearParent, parentId, allowEmpty, title, query, wholeWord, limit, bodies, section, offset, link, { requestId, expectedRevision });
136
+ return buildDocsCommand(values, action, docRef, docRefs, visibility, clearParent, parentId, allowEmpty, title, query, wholeWord, limit, bodies, section, offset, link, { requestId, expectedRevision }, parseDocsRecoveryFlags(action, values, docRef));
81
137
  }
82
138
  /**
83
139
  * BLI-10450: `read` takes either several document references OR
@@ -129,7 +185,7 @@ function parseDocsAction(positionals) {
129
185
  const first = positionals[0];
130
186
  const action = (first === undefined ? "list" : first);
131
187
  if (!DOCS_ACTIONS.has(action)) {
132
- throw new Error(`Unknown docs command: ${first}. Try list, tree, read, create, update, delete, link, sync-status, export, mutation-status, share, or access.`);
188
+ throw new Error(`Unknown docs command: ${first}. Try list, search, tree, read, create, update, delete, link, sync-status, export, mutation-status, share, access, trash, restore, versions, favorite, unfavorite, marks, comments, or comment.`);
133
189
  }
134
190
  return { action, rest: positionals.slice(first === undefined ? 0 : 1) };
135
191
  }
@@ -188,8 +244,8 @@ function parseDocsWriteFlags(action, values) {
188
244
  throw new Error("--allow-empty belongs to `cockpit docs update`.");
189
245
  }
190
246
  // BLI-21298: the write-safety pair belongs to `update`, like the other save-shaping flags.
191
- if ((values.flags.has("--request-id") || values.flags.has("--expected-revision")) && action !== "update") {
192
- throw new Error("--request-id and --expected-revision belong to `cockpit docs update`.");
247
+ if ((values.flags.has("--request-id") || values.flags.has("--expected-revision")) && action !== "update" && action !== "restore") {
248
+ throw new Error("--request-id and --expected-revision belong to `cockpit docs update` and `cockpit docs restore`.");
193
249
  }
194
250
  const requestId = optionalNonEmpty(values.flags.get("--request-id"));
195
251
  if (requestId && !UUID_PATTERN.test(requestId))
@@ -222,8 +278,8 @@ function parseDocsReadFlags(action, values) {
222
278
  // exclusive (`validateDocsReadTargets`), so the flag is never ambiguous in
223
279
  // any one call, only overloaded across the command's two shapes.
224
280
  const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
225
- if (limit !== undefined && action !== "list" && action !== "read") {
226
- throw new Error("--limit belongs to `cockpit docs list` or `cockpit docs read`.");
281
+ if (limit !== undefined && action !== "list" && action !== "read" && action !== "trash" && action !== "versions") {
282
+ throw new Error("--limit belongs to `cockpit docs list`, `read`, `trash` or `versions`.");
227
283
  }
228
284
  // BLI-10450: `--bodies` pairs with `--parent` to bulk-read a folder;
229
285
  // `--section`/`--offset` slice an explicit reference's body, mirroring
@@ -259,6 +315,38 @@ function parseDocsShareFlags(action, values) {
259
315
  }
260
316
  return { ...(addPerson ? { addPerson } : {}), ...(removePerson ? { removePerson } : {}), ...(reason ? { reason } : {}) };
261
317
  }
318
+ /**
319
+ * BLI-21413: `docs trash [--cursor C] [--limit N]`, `docs versions <doc> [--before N] [--limit N] [--version V]`,
320
+ * `docs restore <id>` (take a page out of the trash) and `docs restore <doc> --version V [--expected-revision N]`.
321
+ */
322
+ function parseDocsRecoveryFlags(action, values, docRef) {
323
+ const cursor = optionalNonEmpty(values.flags.get("--cursor"));
324
+ const before = optionalPositiveInteger(values.flags.get("--before"), "--before");
325
+ const versionId = optionalNonEmpty(values.flags.get("--version"));
326
+ if (cursor && action !== "trash" && action !== "list")
327
+ throw new Error("--cursor belongs to `cockpit docs trash` and `cockpit docs list`.");
328
+ const all = values.booleans.has("--all");
329
+ if (all && action !== "list")
330
+ throw new Error("--all belongs to `cockpit docs list`.");
331
+ if (all && cursor)
332
+ throw new Error("docs list takes --all (walk every page) or --cursor (one page), not both.");
333
+ if (before !== undefined && action !== "versions")
334
+ throw new Error("--before belongs to `cockpit docs versions`.");
335
+ if (versionId && action !== "restore" && action !== "versions")
336
+ throw new Error("--version belongs to `cockpit docs restore` and `cockpit docs versions`.");
337
+ if (versionId && !UUID_PATTERN.test(versionId))
338
+ throw new Error("--version must be a version id (a UUID, from `cockpit docs versions <doc>`).");
339
+ if (action === "restore" && !versionId) {
340
+ if (!UUID_PATTERN.test(docRef ?? ""))
341
+ throw new Error("docs restore needs the id of a page in the trash (`cockpit docs trash` lists them), or --version <id> to restore a version of a live page.");
342
+ if (values.flags.has("--expected-revision"))
343
+ throw new Error("--expected-revision belongs to `docs restore --version`; taking a page out of the trash has no revision to check.");
344
+ }
345
+ if (action === "versions" && versionId && (before !== undefined || values.flags.has("--limit"))) {
346
+ throw new Error("docs versions --version reads one version; --before and --limit page the list.");
347
+ }
348
+ return { ...(cursor ? { cursor } : {}), ...(all ? { all } : {}), ...(before === undefined ? {} : { before }), ...(versionId ? { versionId } : {}) };
349
+ }
262
350
  function parseDocsOffset(raw) {
263
351
  const offsetRaw = optionalNonEmpty(raw);
264
352
  if (offsetRaw === undefined)
@@ -268,7 +356,7 @@ function parseDocsOffset(raw) {
268
356
  throw new Error("--offset must be a whole number 0 or greater.");
269
357
  return offset;
270
358
  }
271
- function buildDocsCommand(values, action, docRef, docRefs, visibility, clearParent, parentId, allowEmpty, title, query, wholeWord, limit, bodies, section, offset, link, safety) {
359
+ function buildDocsCommand(values, action, docRef, docRefs, visibility, clearParent, parentId, allowEmpty, title, query, wholeWord, limit, bodies, section, offset, link, safety, recovery) {
272
360
  return {
273
361
  kind: "docs",
274
362
  action,
@@ -295,6 +383,7 @@ function buildDocsCommand(values, action, docRef, docRefs, visibility, clearPare
295
383
  ...(link.clearLink ? { clearLink: true } : {}),
296
384
  ...parseDocsExportFlags(action, values),
297
385
  ...parseDocsShareFlags(action, values),
386
+ ...recovery,
298
387
  json: values.booleans.has("--json"),
299
388
  };
300
389
  }
@@ -1,7 +1,14 @@
1
1
  import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
2
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
+ import { MSG_CHANNEL_BOOLEAN_FLAGS, MSG_CHANNEL_VALUE_FLAGS, isMsgChannelAction, msgChannelNormalizeArgs, parseMsgChannelArgs, rejectMsgChannelFlags } from "./msg-channel-args.js";
4
+ import { MSG_CONTEXT_VALUE_FLAGS, parseMsgContextArgs, rejectMsgContextFlags } from "./msg-context-args.js";
5
+ import { MSG_MARK_VALUE_FLAGS, isMsgMarkAction, parseMsgMarkArgs, rejectMsgMarkFlags } from "./msg-mark-args.js";
6
+ import { MSG_SCHEDULE_VALUE_FLAGS, isMsgScheduleAction, parseMsgScheduleArgs, rejectMsgScheduleFlags } from "./msg-schedule-args.js";
7
+ import { MSG_NOTIFY_ONLY_FLAGS, parseMsgNotifyArgs } from "./msg-notify-args.js";
8
+ import { MSG_SEARCH_BOOLEAN_FLAGS, MSG_SEARCH_ONLY_FLAGS, MSG_SEARCH_VALUE_FLAGS, readMsgSearchFilters } from "./msg-search-args.js";
9
+ const MSG_FILE_VERBS = new Set(["upload", "show", "download", "cancel", "list"]);
3
10
  const MSG_DRAFT_VERBS = new Set(["list", "show", "save", "discard", "restore", "send"]);
4
- const MSG_ACTIONS = new Set(["channels", "read", "send", "thread", "create", "dm", "show", "unread", "edit", "delete", "outcome", "draft"]);
11
+ const MSG_ACTIONS = new Set(["channels", "read", "send", "thread", "create", "dm", "show", "unread", "edit", "delete", "outcome", "draft", "file", "activity", "follow", "unfollow", "react", "notify", "search", "channel", "members", "hide", "unhide", "save", "saved", "pin", "pins", "forward", "schedule", "remind", "scheduled", "context"]);
5
12
  const MSG_ACTIONS_NEEDING_A_CHANNEL = new Set(["read", "send", "unread"]);
6
13
  /**
7
14
  * `--members a@x.test,b@y.test` — addresses only, split on commas, never
@@ -25,7 +32,8 @@ function parseMemberEmails(raw) {
25
32
  }
26
33
  return emails;
27
34
  }
28
- export function parseMsgArgs(args) {
35
+ export function parseMsgArgs(rawArgs) {
36
+ const args = msgChannelNormalizeArgs(rawArgs);
29
37
  const values = parseNamedArgs(args, {
30
38
  allowedFlags: [
31
39
  "--home",
@@ -39,18 +47,78 @@ export function parseMsgArgs(args) {
39
47
  "--description",
40
48
  "--from",
41
49
  "--mark-read",
50
+ "--mentions",
51
+ "--threads",
52
+ "--unread",
53
+ "--remove",
42
54
  "--request-id",
43
55
  "--expected-revision",
56
+ "--name",
57
+ "--out",
58
+ "--file",
59
+ "--level",
60
+ "--timezone",
61
+ "--message",
62
+ "--mute",
63
+ "--unmute",
64
+ "--clear",
65
+ "--off",
66
+ ...MSG_SEARCH_BOOLEAN_FLAGS,
67
+ ...MSG_SEARCH_VALUE_FLAGS,
68
+ ...MSG_CHANNEL_VALUE_FLAGS,
69
+ ...MSG_CHANNEL_BOOLEAN_FLAGS,
70
+ ...MSG_MARK_VALUE_FLAGS,
71
+ ...MSG_SCHEDULE_VALUE_FLAGS,
72
+ ...MSG_CONTEXT_VALUE_FLAGS,
44
73
  "--json",
45
74
  ],
46
75
  valueFlags: [
47
76
  "--home", "--dashboard-url", "--channel", "--thread", "--limit", "--members", "--description", "--before", "--from",
48
- "--request-id", "--expected-revision",
77
+ "--request-id", "--expected-revision", "--name", "--out", "--file",
78
+ "--level", "--timezone", "--message", ...MSG_SEARCH_VALUE_FLAGS, ...MSG_CHANNEL_VALUE_FLAGS, ...MSG_MARK_VALUE_FLAGS, ...MSG_SCHEDULE_VALUE_FLAGS, ...MSG_CONTEXT_VALUE_FLAGS,
49
79
  ],
50
80
  });
51
81
  const { action, rest } = parseMsgAction(values.positionals);
82
+ if (action === "context")
83
+ return parseMsgContextArgs(values, rest);
84
+ if (isMsgChannelAction(action))
85
+ return parseMsgChannelArgs(action, values, rest);
86
+ rejectMsgContextFlags(values);
87
+ if (isMsgMarkAction(action))
88
+ return parseMsgMarkArgs(action, values, rest);
89
+ if (isMsgScheduleAction(action))
90
+ return parseMsgScheduleArgs(action, values, rest);
91
+ rejectMsgChannelFlags(values);
92
+ rejectMsgMarkFlags(values);
93
+ rejectMsgScheduleFlags(values);
94
+ if (action === "search")
95
+ return parseMsgSearchArgs(values, rest);
96
+ const searchFlag = MSG_SEARCH_ONLY_FLAGS.find((flag) => values.flags.has(flag) || values.booleans.has(flag));
97
+ if (searchFlag)
98
+ throw new Error(`${searchFlag} belongs to \`cockpit msg search\`.`);
99
+ if (action === "notify")
100
+ return parseMsgNotifyArgs(values, rest);
101
+ const notifyFlag = MSG_NOTIFY_ONLY_FLAGS.find((flag) => values.flags.has(flag) || values.booleans.has(flag));
102
+ if (notifyFlag)
103
+ throw new Error(`${notifyFlag} belongs to \`cockpit msg notify\`.`);
52
104
  if (action === "draft")
53
105
  return parseMsgDraftArgs(values, rest);
106
+ if (action === "file")
107
+ return parseMsgFileArgs(values, rest);
108
+ if (action === "activity" || action === "follow" || action === "unfollow" || action === "react")
109
+ return parseMsgActivityArgs(action, values, rest);
110
+ for (const flag of ["--mentions", "--threads", "--unread", "--remove"]) {
111
+ if (values.booleans.has(flag))
112
+ throw new Error(`${flag} belongs to \`cockpit msg activity\` or \`msg react\`.`);
113
+ }
114
+ const fileIds = values.flagValues.get("--file") ?? [];
115
+ if (fileIds.length > 0 && action !== "send")
116
+ throw new Error("--file belongs to `cockpit msg send`.");
117
+ if (fileIds.length > 10 || fileIds.some((id) => !UUID_PATTERN.test(id))) {
118
+ throw new Error("--file takes a file UUID from `cockpit msg file upload`, at most 10 per message.");
119
+ }
120
+ if (values.flags.has("--name") || values.flags.has("--out"))
121
+ throw new Error("--name and --out belong to `cockpit msg file`.");
54
122
  if (values.flags.has("--before") && action !== "read" && action !== "thread") {
55
123
  throw new Error("--before belongs to msg read or msg thread.");
56
124
  }
@@ -59,7 +127,152 @@ export function parseMsgArgs(args) {
59
127
  const { isPrivate, memberEmails, description } = parseMsgCreateFlags(action, values);
60
128
  const { channelRef, threadId, limit } = parseMsgReadTarget(action, rest, values);
61
129
  const writeSafety = parseMsgWriteSafety(action, rest, values);
62
- return buildMsgCommand(values, action, channelRef, channelName, dmEmail, memberEmails, description, isPrivate, threadId, limit, markRead, unreadFrom, writeSafety);
130
+ const built = buildMsgCommand(values, action, channelRef, channelName, dmEmail, memberEmails, description, isPrivate, threadId, limit, markRead, unreadFrom, writeSafety);
131
+ // `msg dm a@x b@y` is a group message: the single-address shape (dmEmail) is unchanged (BLI-21685).
132
+ const withGroup = action === "dm" && rest.length > 1 ? { ...built, dmEmails: rest } : built;
133
+ return fileIds.length > 0 ? { ...withGroup, fileIds } : withGroup;
134
+ }
135
+ /**
136
+ * `cockpit msg search [words] [--from <email|me>]... [--in <channel>] [--since <day>] [--until <day>] [--timezone <zone>]
137
+ * [--replies|--top-level] [--has-file] [--before <cursor>] [--limit <n>]` (BLI-21658). The words are positional and may
138
+ * be empty when a filter is set; inline operators (`from:`, `in:`, `has:file`) work inside them as in the web box.
139
+ */
140
+ function parseMsgSearchArgs(values, rest) {
141
+ const stray = ["--channel", "--thread", "--members", "--description", "--request-id", "--expected-revision", "--name", "--out", "--file", "--level", "--message"]
142
+ .find((flag) => values.flags.has(flag));
143
+ const strayBoolean = ["--private", "--mark-read", "--mentions", "--threads", "--unread", "--remove", "--mute", "--unmute", "--clear", "--off"]
144
+ .find((flag) => values.booleans.has(flag));
145
+ if (stray || strayBoolean)
146
+ throw new Error(`${stray ?? strayBoolean} does not belong to \`cockpit msg search\`.`);
147
+ const searchFilters = readMsgSearchFilters(values);
148
+ const searchQuery = rest.join(" ").trim();
149
+ const filtered = Boolean(searchFilters.from || searchFilters.in || searchFilters.since || searchFilters.until || searchFilters.thread || searchFilters.hasFile);
150
+ if (searchQuery === "" && !filtered) {
151
+ throw new Error("msg search needs words or a filter, e.g. `cockpit msg search deploy --from me` or `cockpit msg search --has-file`.");
152
+ }
153
+ const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
154
+ if (limit !== undefined && limit > 100)
155
+ throw new Error("--limit takes 1 to 100.");
156
+ const before = optionalNonEmpty(values.flags.get("--before"));
157
+ return {
158
+ kind: "msg",
159
+ action: "search",
160
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
161
+ dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
162
+ isPrivate: false,
163
+ searchQuery,
164
+ searchFilters,
165
+ ...(before ? { before } : {}),
166
+ ...(limit === undefined ? {} : { limit }),
167
+ json: values.booleans.has("--json"),
168
+ };
169
+ }
170
+ /**
171
+ * `cockpit msg activity [--mentions|--threads] [--unread] [--before <cursor>] [--limit <n>] [--mark-read]`,
172
+ * `follow <message-id>`, `unfollow <message-id>`, `react <message-id> <emoji> [--remove]` (BLI-21598).
173
+ */
174
+ function parseMsgActivityArgs(action, values, rest) {
175
+ const stray = ["--channel", "--thread", "--members", "--description", "--from", "--request-id", "--expected-revision", "--name", "--out", "--file"]
176
+ .find((flag) => values.flags.has(flag));
177
+ if (stray || values.booleans.has("--private")) {
178
+ throw new Error(`${stray ?? "--private"} does not belong to \`cockpit msg ${action}\`.`);
179
+ }
180
+ const mentions = values.booleans.has("--mentions");
181
+ const threads = values.booleans.has("--threads");
182
+ const unreadOnly = values.booleans.has("--unread");
183
+ const remove = values.booleans.has("--remove");
184
+ const markRead = values.booleans.has("--mark-read");
185
+ const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
186
+ const before = optionalNonEmpty(values.flags.get("--before"));
187
+ const base = {
188
+ kind: "msg",
189
+ action,
190
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
191
+ dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
192
+ isPrivate: false,
193
+ json: values.booleans.has("--json"),
194
+ };
195
+ if (action === "activity") {
196
+ if (rest.length > 0)
197
+ throw new Error(`msg activity does not take "${rest[0]}".`);
198
+ if (mentions && threads)
199
+ throw new Error("Pick one of --mentions or --threads, or neither for both.");
200
+ if (remove)
201
+ throw new Error("--remove belongs to `cockpit msg react`.");
202
+ return {
203
+ ...base,
204
+ ...(mentions ? { activityKind: "mention" } : threads ? { activityKind: "reply" } : {}),
205
+ ...(unreadOnly ? { unreadOnly } : {}),
206
+ ...(before ? { before } : {}),
207
+ ...(limit === undefined ? {} : { limit }),
208
+ ...(markRead ? { markRead } : {}),
209
+ };
210
+ }
211
+ if (mentions || threads || unreadOnly || markRead || before || limit !== undefined) {
212
+ throw new Error(`Only \`cockpit msg activity\` takes --mentions, --threads, --unread, --mark-read, --before or --limit.`);
213
+ }
214
+ if (remove && action !== "react")
215
+ throw new Error("--remove belongs to `cockpit msg react`.");
216
+ const targetId = optionalNonEmpty(rest[0]);
217
+ if (!targetId || !UUID_PATTERN.test(targetId)) {
218
+ throw new Error(`msg ${action} needs a message id (a UUID), e.g. \`cockpit msg ${action} <message-id>${action === "react" ? " :+1:" : ""}\`.`);
219
+ }
220
+ if (action !== "react") {
221
+ if (rest.length > 1)
222
+ throw new Error(`msg ${action} takes one message id, not ${rest.length}.`);
223
+ return { ...base, targetId };
224
+ }
225
+ const emoji = optionalNonEmpty(rest[1]);
226
+ if (!emoji || emoji.length > 32 || rest.length > 2) {
227
+ throw new Error("msg react needs one emoji of at most 32 characters, e.g. `cockpit msg react <message-id> 👍`.");
228
+ }
229
+ return { ...base, targetId, emoji, ...(remove ? { remove } : {}) };
230
+ }
231
+ /**
232
+ * `cockpit msg file upload <channel> <path|-> [--name <n>]`, `show <id>`, `download <id> [--out <path>]`,
233
+ * `cancel <id>`, `list <channel>` (BLI-21521). Stdin upload (`-`) needs --name.
234
+ */
235
+ function parseMsgFileArgs(values, rest) {
236
+ const verb = rest[0];
237
+ if (!verb || !MSG_FILE_VERBS.has(verb)) {
238
+ throw new Error(`Unknown msg file command: ${rest[0] ?? "(none)"}. Try upload, show, download, cancel or list.`);
239
+ }
240
+ const stray = ["--limit", "--before", "--members", "--description", "--from", "--channel", "--thread", "--request-id", "--expected-revision", "--file"]
241
+ .find((flag) => values.flags.has(flag));
242
+ if (stray || values.booleans.has("--private") || values.booleans.has("--mark-read")) {
243
+ throw new Error(`${stray ?? "That flag"} does not belong to \`cockpit msg file\`. It takes --name (upload) and --out (download).`);
244
+ }
245
+ const positional = rest.slice(1);
246
+ const wanted = verb === "upload" ? 2 : 1;
247
+ if (positional.length !== wanted) {
248
+ throw new Error(verb === "upload"
249
+ ? "msg file upload needs a channel and a path (or - for stdin), e.g. `cockpit msg file upload general ./report.pdf`."
250
+ : `msg file ${verb} needs ${verb === "list" ? "a channel" : "a file id"}, e.g. \`cockpit msg file ${verb} ${verb === "list" ? "general" : "<file-id>"}\`.`);
251
+ }
252
+ const name = optionalNonEmpty(values.flags.get("--name"));
253
+ const out = optionalNonEmpty(values.flags.get("--out"));
254
+ if (name && verb !== "upload")
255
+ throw new Error("--name belongs to `cockpit msg file upload`.");
256
+ if (out && verb !== "download")
257
+ throw new Error("--out belongs to `cockpit msg file download`.");
258
+ if (verb === "upload" && positional[1] === "-" && !name)
259
+ throw new Error("Uploading from stdin needs --name <file name>.");
260
+ if ((verb === "show" || verb === "download" || verb === "cancel") && !UUID_PATTERN.test(positional[0])) {
261
+ throw new Error(`msg file ${verb} takes a file UUID.`);
262
+ }
263
+ return {
264
+ kind: "msg",
265
+ action: "file",
266
+ fileVerb: verb,
267
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
268
+ dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
269
+ ...(verb === "upload" || verb === "list" ? { channelRef: positional[0] } : { targetId: positional[0] }),
270
+ ...(verb === "upload" ? { filePath: positional[1] } : {}),
271
+ ...(name ? { fileName: name } : {}),
272
+ ...(out ? { outPath: out } : {}),
273
+ isPrivate: false,
274
+ json: values.booleans.has("--json"),
275
+ };
63
276
  }
64
277
  /**
65
278
  * `cockpit msg draft [list|show|save|discard|restore|send] <channel> [--thread <id>] [--expected-revision <n>]`
@@ -159,7 +372,7 @@ function parseMsgAction(positionals) {
159
372
  const first = positionals[0];
160
373
  const action = (first === undefined ? "channels" : first);
161
374
  if (!MSG_ACTIONS.has(action)) {
162
- 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; for unsent text: draft.`);
375
+ 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; for unsent text: draft; for files: file; to keep or share a message: save, saved, pin, pins or forward; to send or be reminded later: schedule, remind or scheduled; for Tower docs, tickets, events and notes on a message: context.`);
163
376
  }
164
377
  return { action, rest: positionals.slice(first === undefined ? 0 : 1) };
165
378
  }
@@ -182,11 +395,10 @@ function parseMsgCreateTarget(action, rest) {
182
395
  else if (action === "dm") {
183
396
  dmEmail = optionalNonEmpty(rest[0]);
184
397
  if (!dmEmail)
185
- throw new Error("msg dm needs the person's email address, e.g. `cockpit msg dm ada@example.com`.");
186
- if (rest.length > 1)
187
- throw new Error(`msg dm takes one email address, not ${rest.length}.`);
188
- if (!dmEmail.includes("@"))
189
- throw new Error(`msg dm takes an email address; "${dmEmail}" is not one.`);
398
+ throw new Error("msg dm needs the person's email address, e.g. `cockpit msg dm ada@example.com` (several for a group message).");
399
+ const notAnAddress = rest.find((email) => !email.includes("@"));
400
+ if (notAnAddress !== undefined)
401
+ throw new Error(`msg dm takes email addresses; "${notAnAddress}" is not one.`);
190
402
  }
191
403
  return { channelName, dmEmail };
192
404
  }