@bli-cockpit/cli 0.2.242 → 0.2.243

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/dist/adapters/git-state.js +1 -0
  2. package/dist/adapters/raw-evidence-collection-state.js +22 -11
  3. package/dist/adapters/raw-evidence-git-diff.js +1 -0
  4. package/dist/agent-rules.js +8 -0
  5. package/dist/autostart-contract.js +12 -0
  6. package/dist/autostart-darwin-plist.js +2 -2
  7. package/dist/autostart-darwin.js +1 -1
  8. package/dist/autostart-remind.js +245 -0
  9. package/dist/autostart-self-heal.js +17 -1
  10. package/dist/autostart-windows-scripts.js +7 -7
  11. package/dist/autostart.js +16 -15
  12. package/dist/commands/approvals-check-where.js +14 -0
  13. package/dist/commands/approvals-render.js +8 -1
  14. package/dist/commands/approvals.js +3 -2
  15. package/dist/commands/autostart-command.js +10 -0
  16. package/dist/commands/autostart-heal.js +36 -1
  17. package/dist/commands/brain.js +1 -1
  18. package/dist/commands/cal-agenda.js +44 -0
  19. package/dist/commands/cal-availability-share.js +97 -0
  20. package/dist/commands/cal-booking.js +147 -0
  21. package/dist/commands/cal-join.js +62 -0
  22. package/dist/commands/cal-links.js +68 -0
  23. package/dist/commands/cal-people.js +136 -0
  24. package/dist/commands/cal-remind.js +176 -0
  25. package/dist/commands/cal-reminders.js +33 -0
  26. package/dist/commands/cal-suggest.js +89 -0
  27. package/dist/commands/cal.js +41 -1
  28. package/dist/commands/cycle-args.js +127 -0
  29. package/dist/commands/cycles.js +117 -0
  30. package/dist/commands/docs-comments-args.js +85 -0
  31. package/dist/commands/docs-comments.js +128 -0
  32. package/dist/commands/docs-files-args.js +49 -0
  33. package/dist/commands/docs-files.js +179 -0
  34. package/dist/commands/docs-formatting.js +45 -0
  35. package/dist/commands/docs-inbox-args.js +93 -0
  36. package/dist/commands/docs-inbox.js +126 -0
  37. package/dist/commands/docs-notion.js +5 -0
  38. package/dist/commands/docs-read.js +2 -1
  39. package/dist/commands/docs-recovery.js +173 -0
  40. package/dist/commands/docs-refs-args.js +104 -0
  41. package/dist/commands/docs-refs.js +153 -0
  42. package/dist/commands/docs-search-args.js +79 -0
  43. package/dist/commands/docs-search.js +64 -0
  44. package/dist/commands/docs-sync-args.js +82 -0
  45. package/dist/commands/docs-sync.js +197 -0
  46. package/dist/commands/docs-tree.js +136 -0
  47. package/dist/commands/docs.js +85 -51
  48. package/dist/commands/doctor-update.js +4 -1
  49. package/dist/commands/inbox-args.js +81 -0
  50. package/dist/commands/inbox.js +88 -0
  51. package/dist/commands/issue-batch.js +61 -0
  52. package/dist/commands/issue-relations.js +151 -0
  53. package/dist/commands/issue-write.js +38 -5
  54. package/dist/commands/issue.js +32 -1
  55. package/dist/commands/local-args-tower-approvals.js +16 -4
  56. package/dist/commands/local-args-tower-cal-booking.js +81 -0
  57. package/dist/commands/local-args-tower-cal-links.js +42 -0
  58. package/dist/commands/local-args-tower-cal-remind.js +74 -0
  59. package/dist/commands/local-args-tower-cal-share.js +41 -0
  60. package/dist/commands/local-args-tower-cal.js +81 -8
  61. package/dist/commands/local-args-tower-docs-msg.js +98 -9
  62. package/dist/commands/local-args-tower-msg.js +222 -10
  63. package/dist/commands/local-args-tower-search.js +32 -5
  64. package/dist/commands/local-args-tower-work.js +110 -34
  65. package/dist/commands/local-auth.js +1 -1
  66. package/dist/commands/local-help-commands-tower-ops.js +2 -0
  67. package/dist/commands/local-help-commands-tower.js +47 -31
  68. package/dist/commands/local-help-cycles.js +9 -0
  69. package/dist/commands/local-help-docs-files.js +10 -0
  70. package/dist/commands/local-help-docs-formatting.js +7 -0
  71. package/dist/commands/local-help-docs-inbox.js +10 -0
  72. package/dist/commands/local-help-docs-recovery.js +10 -0
  73. package/dist/commands/local-help-docs-refs.js +11 -0
  74. package/dist/commands/local-help-docs-sync.js +9 -0
  75. package/dist/commands/local-help-inbox.js +9 -0
  76. package/dist/commands/local-help-project.js +21 -0
  77. package/dist/commands/local-help-templates.js +9 -0
  78. package/dist/commands/local-help-views.js +10 -0
  79. package/dist/commands/local-help-work-notify.js +6 -0
  80. package/dist/commands/local-help.js +2 -2
  81. package/dist/commands/mcp-stdio-probe.js +1 -0
  82. package/dist/commands/msg-activity.js +134 -0
  83. package/dist/commands/msg-channel-args.js +92 -0
  84. package/dist/commands/msg-channel.js +125 -0
  85. package/dist/commands/msg-context-args.js +67 -0
  86. package/dist/commands/msg-context.js +65 -0
  87. package/dist/commands/msg-dm.js +28 -0
  88. package/dist/commands/msg-file.js +185 -0
  89. package/dist/commands/msg-mark-args.js +78 -0
  90. package/dist/commands/msg-mark.js +148 -0
  91. package/dist/commands/msg-notify-args.js +100 -0
  92. package/dist/commands/msg-notify.js +122 -0
  93. package/dist/commands/msg-schedule-args.js +114 -0
  94. package/dist/commands/msg-schedule.js +226 -0
  95. package/dist/commands/msg-search-args.js +71 -0
  96. package/dist/commands/msg-search.js +48 -0
  97. package/dist/commands/msg-write.js +15 -1
  98. package/dist/commands/msg.js +62 -39
  99. package/dist/commands/project-args.js +82 -0
  100. package/dist/commands/project.js +71 -7
  101. package/dist/commands/public-root.js +1 -1
  102. package/dist/commands/search.js +23 -0
  103. package/dist/commands/session-sync-failures.js +17 -4
  104. package/dist/commands/session-sync-health.js +7 -2
  105. package/dist/commands/session-sync-hold.js +42 -1
  106. package/dist/commands/session-sync-upload.js +2 -0
  107. package/dist/commands/session-sync.js +1 -0
  108. package/dist/commands/sync-doctor.js +14 -1
  109. package/dist/commands/sync-report.js +1 -1
  110. package/dist/commands/template-args.js +122 -0
  111. package/dist/commands/templates.js +162 -0
  112. package/dist/commands/view-args.js +131 -0
  113. package/dist/commands/views.js +138 -0
  114. package/dist/commands/work-notify-args.js +67 -0
  115. package/dist/commands/work-notify.js +63 -0
  116. package/dist/delivery-hold-notice.js +34 -0
  117. package/dist/log-rotation.js +2 -2
  118. package/dist/notifier.js +83 -0
  119. package/dist/process-runner.js +4 -0
  120. package/dist/repo-identity-git.js +1 -0
  121. package/package.json +4 -4
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The `cockpit docs trash|restore|versions` lines of the docs `--help` (BLI-21413), split from
3
+ * `local-help-commands-tower.ts` to keep that file under the readability ratchet.
4
+ */
5
+ export const DOCS_RECOVERY_HELP = [
6
+ "trash [--cursor <c>] [--limit <n>] — the deleted pages YOU may restore (the ones you wrote; a super_admin sees all), newest deletion first, 50 at a time. A reader of someone else's deleted page is shown nothing. If more follow, the answer carries next_cursor: pass it as --cursor. --json shape: { documents: [{ id, title, slug, visibility, deleted_at }], next_cursor, complete }.",
7
+ "restore <id> [--request-id <uuid>] — takes a page out of the trash (BLI-21413). The id is the one `docs trash` printed. The page keeps its id; if a live page now holds its slug it comes back at the next free slug-N and the answer says so (slug_changed, old_slug, new_slug). Author or super_admin only: anyone else gets not_restore_manager; a live page is not_in_trash. --json shape: { ok, document_id, slug, slug_changed, old_slug, new_slug, mutation }.",
8
+ "versions <id|slug> [--before <n>] [--limit <n>] — the page's saved versions, newest first, METADATA ONLY (id, number, who, when, origin), 50 at a time (--limit max 200), with the exact total and next_before. `versions <id|slug> --version <version-id>` prints that ONE version's body. --json shape: { versions, next_before, total, permission }.",
9
+ "restore <id|slug> --version <version-id> [--expected-revision <n>] [--request-id <uuid>] — makes that version the page's current text through the ordinary atomic save (what was there is kept as a version) and records a version_restored event. A stale --expected-revision writes nothing and answers revision_conflict (--json: mutation.current); a reader gets not_page_editor. --json shape: { document, mutation }.",
10
+ ];
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The `cockpit docs templates|create --template|refs|review|maintenance` lines of the docs `--help` (BLI-21849), split from
3
+ * `local-help-commands-tower.ts` to keep that file under the readability ratchet.
4
+ */
5
+ export const DOCS_REFS_HELP = [
6
+ "templates — the curated page templates: the pages directly under the top-level page whose address is `templates`, that you can read. With no such page it says how to make one. --json shape: { ok, templates_page_id, templates: [{ id, title, slug, revision, updated_at, body_chars }], coverage: { returned, coverage: complete|capped }, hint }.",
7
+ "create --template <id|slug> --title <title> [--parent <id>] [--visibility org|private] — makes a new page whose body is a copy of that template's CURRENT text (files stay the template's), and records the template and its revision on the new page. A body is not accepted beside a template (template_and_body); a page that is not one of the templates is template_not_found_or_unreadable. If the page was made but the template link could not be written the answer says provenance_recorded: false. --json shape: { ok, document, template: { id, revision }, provenance_recorded }.",
8
+ "refs <id|slug> — links from and to a page, derived from saved link targets (/docs/<id>, /tickets/<ID>, /meeting-notes/<id>; a slug link is not indexed, an id link survives a rename). \"Linked from\" lists only pages you can read now (another person's private page never appears; there is no hidden count). A ticket or note you cannot read prints as \"Not available to you\" with no title. A link grants nothing. --json shape: { ok, document_id, backlinks: [{ id, title, slug, updated_at, href }], outgoing: [{ kind, key, title, href } | { kind, key, state: \"unavailable\" }], coverage: { backlinks: { returned, coverage }, outgoing: { listed, returned, unavailable, coverage } } }.",
9
+ "review <id|slug> [--owner <email|me|none>] [--every <days>|--no-schedule] [--reviewed] — a page's owner and review schedule; with a flag it changes them (page editors only: stewardship_write_refused otherwise). --owner takes a team member's exact email, me or none (owner_not_member for anyone else); --every is 1 to 730 days (invalid_review_schedule); --reviewed stamps the time and you as the reviewer from Tower's own clock, never a date you give. State is no_schedule, ok or review_due (computed when read; nothing is approved for you). --json shape: { ok, stewardship: { document_id, recorded, owner, review_every_days, review_due_at, last_reviewed_at, last_reviewed_by, created_from_template_id, created_from_template_revision, state } }.",
10
+ "maintenance [--mine] [--state review_due|unowned|all] — pages that need a person: due for review, with no owner (default all), or with --mine everything you own. Reads on your own access; coverage says complete or capped. --json shape: { ok, state, items: [{ id, title, slug, owner, review_every_days, review_due_at, last_reviewed_at, state, reasons }], coverage: { scanned, returned, coverage } }.",
11
+ ];
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The `cockpit docs sync-status <page>` detail, `sync-resolve` and `manifest` lines of the docs `--help` (BLI-21875), split
3
+ * from `local-help-commands-tower.ts` to keep that file under the readability ratchet.
4
+ */
5
+ export const DOCS_SYNC_HELP = [
6
+ "sync-status <id|slug> — one linked page's Notion sync, from what the last push and pull passes RECORDED (Tower never asks Notion from a request): the state sentence, the newest push and pull receipt each with its reason label and age, what could not be carried (\"1 file kept only as a link\", blocks missing, content Tower cannot write back), the conflicts (versions a sync kept, open or resolved, newest 50 with a total) and the decisions made. A page Notion no longer shares reads \"Notion no longer shares this page\", not synced. --json adds { detail: { can_edit, state_sentence, last_receipt: { push, pull }, losses, conflicts: { total, open, shown, items: [{ version_id, version_number, origin, status, sentence, resolution }] }, resolutions, resolve_note, incomplete, as_of } }.",
7
+ "sync-resolve <id|slug> --version <id> --keep-current|--use-kept [--expected-revision <n>] [--request-id <uuid>] — decide one conflict; you choose, Tower never picks. --keep-current records the decision and writes no text. --use-kept makes the kept version the page's current text through a version restore (a new history row, revision +1) over your --expected-revision (the page revision you read; stale = revision_conflict, nothing is written or recorded), then records the decision. One decision per kept version: a repeat answers already_resolved with the stored one and writes nothing. Refusals: resolution_write_refused (not a page editor), not_a_sync_conflict (an ordinary version), version_not_found, invalid_resolution. The next push sends the restored text to Notion through the same checks as any push; a page Notion holds content on that Tower cannot write stays protected. --json shape: { ok, outcome: resolved|already_resolved, document_id, resolution: { version_id, choice, resolved_by, resolved_at, resulting_revision }, revision, body_written, note }.",
8
+ "manifest [--under <id|slug>] [--cursor <c>] [--all] [--out <file>] — the library export manifest for a Notion cutover review: readiness facts only, no cutover verdict and no Notion turn-off. Per page you can read (another person's private page never appears): id, slug, title, parent, visibility, author, revision, body_sha256, body_chars, Notion link, editor ids (\"not_visible\" when you may not see them), files (id, name, type, byte size, state; no URLs, no bytes) and no body. 2,000 pages per call with next_cursor; --all walks every call (at most 100) and reports requested, returned, unique and coverage exhausted|capped|failed_partial (a failed call keeps the pages already read and prints the cursor to resume). --out writes the full JSON to a new file (never overwrites). --json shape: { ok, entries, requested, returned, unique, coverage: { status, calls, reason? }, coverage_text, failures: [{ stage, reason, count, document_ids }], next_cursor, under, generated_at }.",
9
+ ];
@@ -0,0 +1,9 @@
1
+ /** `cockpit ticket inbox` help (BLI-21703, P9), one line a verb, joined into the `issue` help. */
2
+ export const INBOX_HELP_LINES = [
3
+ "inbox [--unread] [--kind assigned|mentioned|commented] [--before <cursor>] [--limit n] — YOUR inbox, newest first: tickets assigned to you, comments that mention you, and comments on tickets you follow. 50 items by default, at most 100; continue with --before <next_cursor>. --json shape: { items: [{ id, kind, issue_id, identifier, title, state, actor_id, created_at, read_at }], next_cursor, has_more, coverage: { returned, dropped }, counts: { unread, capped } }. An item whose ticket you cannot read is left out and counted in coverage.dropped. Unread counts stop at 500 (capped).",
4
+ "read <notification-id>... | --all — mark your own items read (1 to 200 ids, or every unread item). Another member's id changes nothing. Read state is yours alone.",
5
+ "unread <notification-id>... — mark your own items unread again.",
6
+ "subscribe <id> — follow a ticket (reason manual). You also follow a ticket you create, get assigned, are mentioned on or comment on.",
7
+ "unsubscribe <id> — stop following. An explicit unsubscribe beats auto-follow: you get no more commented items, but a direct assignment or @mention still arrives.",
8
+ "Mention someone in a comment with [@Name](mention:person:<uuid>). Refusals keep Tower's label: inbox_ids_invalid, inbox_too_many_ids, invalid_cursor, invalid_filter, issue_not_found_or_unreadable.",
9
+ ];
@@ -0,0 +1,21 @@
1
+ /** `cockpit project` help (BLI-21659), kept beside the parser in project-args.ts. */
2
+ export const PROJECT_HELP = [
3
+ "project",
4
+ [
5
+ "Usage: cockpit project [list] [--archived] [--limit <n>] [--cursor <c>] [--json]",
6
+ " cockpit project show <id|name> [--json]",
7
+ ' cockpit project create --name "<name>" [--description <text>|--file <path>] [--color <c>] [--request-id <uuid>] [--json]',
8
+ " cockpit project update <id|name> [--name <n>] [--description <text>|none|--file <path>] [--request-id <uuid>] [--expected-updated-at <ts>] [--json]",
9
+ " cockpit project archive|restore <id|name> [--request-id <uuid>] [--expected-updated-at <ts>] [--json]",
10
+ "",
11
+ "The projects tickets are filed under. Bare `cockpit project` lists them.",
12
+ "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.",
13
+ "show <ref> — the project, the first page of its tickets and the filtered ticket-list link. <ref> is an id or an exact name (case-folded, never fuzzy).",
14
+ "create / update — name 1 to 200 characters; `--description none` clears the description; `--file` reads it from a local file. Update changes only the fields you give.",
15
+ "archive / restore — soft: tickets keep their project, the Linear mapping is untouched, nothing is deleted. There is no delete verb.",
16
+ "Every write returns a receipt with delivery tower_only: a project write never reaches Linear and never queues a Linear change. The same --request-id replays the first receipt; with another body it is request_id_reused.",
17
+ "--expected-updated-at is the updated_at you last read (shown by show and --json); a stale one is revision_conflict with the current project and changes nothing.",
18
+ "Refusals keep Tower's label: project_name_required, project_name_taken, project_owned_by_linear (a project imported from Linear is named there; archive and restore still work), project_already_archived, project_not_archived, project_not_found_or_unreadable, revision_conflict, request_id_reused, invalid_body.",
19
+ "--json writes one machine-readable object to stdout; every reason and receipt line stays on stderr.",
20
+ ],
21
+ ];
@@ -0,0 +1,9 @@
1
+ /** `cockpit ticket templates` help (BLI-21779, P11), one line a verb, joined into the `issue` help. */
2
+ export const TEMPLATES_HELP_LINES = [
3
+ "templates [list] [--include-archived] — every ticket template (all members read them), newest definition first by name. --json shape: { templates: [{ id, owner_id, name, description, state, priority, estimate, size, project_id, label_names, version, archived_at, updated_at, mine }], archived_hidden }.",
4
+ "templates show <id|name> — one template. A name is exact and case-insensitive; two templates with one name are template_ref_ambiguous (use the id). --json shape: { template }. templates versions <id|name> — every version, newest first: each save or change is one append-only row. --json shape: { template, versions }.",
5
+ "templates save --name \"<n>\" [--description <text|->] [--state <s>] [--priority 0-4] [--size S|M|L] [--project <name|id>] [--label <name>]... [--request-id <uuid>] — saves a template at version 1. A template holds description TEXT and optional presets, never a title, assignee, due date or parent. `--description -` reads the text from stdin. The request id is the template id: the same --request-id with the same fields returns the one template, another definition under it is request_id_reused. You can keep 50 active templates.",
6
+ "templates update <id|name> [the same flags] [--archive|--restore] [--expected-updated-at <ts>] — only the OWNER (anyone else gets template_owned_by_other); only the fields you give change, `none` clears a preset (--label none clears the labels). Each change makes a new version. A stale --expected-updated-at is revision_conflict with the current template. There is no delete: --archive hides it from apply, --restore brings it back.",
7
+ "templates apply <id|name> --title \"<t>\" [--description <text|->] [--state <s>] [--priority 0-4] [--size S|M|L] [--project <name|id>] [--label <name>]... [--assignee me|<uuid>] [--parent <id>] [--due YYYY-MM-DD] [--request-id <uuid>] — creates a ticket by COPYING the template's values at that moment: a flag you give wins, `none` clears what the template would have copied, and later template changes never touch the ticket. The title is always yours: without --title the answer is template_title_required and nothing is created. The same --request-id returns the same ticket; with a different title it is request_id_reused. An unknown label is refused before anything is written; if the label write fails afterwards the ticket stays and the answer says labels failed. --json shape: { issue, mutation, template: { id, version }, labels: { status: none|applied|failed, names, reason? } }.",
8
+ "Template refusals keep Tower's label: template_name_required, template_name_taken, template_not_found_or_unreadable, template_ref_ambiguous, template_owned_by_other, template_archived, template_limit_reached, template_title_required, template_field_invalid (names the field), label_not_found, request_id_reused, revision_conflict.",
9
+ ];
@@ -0,0 +1,10 @@
1
+ /** `cockpit ticket views` help (BLI-21686, P8), one line a verb, joined into the `issue` help. */
2
+ export const VIEWS_HELP_LINES = [
3
+ "views [list] — My Work (built in, first), your favorites, your own views and the shared ones. --json shape: { my_work, favorites, mine, shared, favorites_hidden }.",
4
+ "views show <id|name> — a view and the first 50 tickets it means FOR YOU (`me` is whoever runs it), in the view's own sort, with the web link. A name is exact and case-insensitive; two views with one name are view_ref_ambiguous (use the id). `my-work` is the built-in preset. --json shape: { view, issues, next_cursor, coverage, href }.",
5
+ "views save --name \"<n>\" [--state <s>] [--assignee me|unassigned|<uuid>] [--project <name|id>] [--label <name>] [--q <text>] [--layout list|board] [--sort updated_at|created_at] [--dir asc|desc] [--shared] [--request-id <uuid>] — saves a QUERY, never rows: `--assignee me` is stored as \"me\" and resolves to each reader. Private unless --shared (then every signed-in member can open it; they still see only tickets they may read). The request id is the view id: the same --request-id with the same definition returns the one view, with another definition it is request_id_reused. You can keep 50 active views.",
6
+ "views update <id|name> [the same flags] [--private] [--archive|--restore] [--expected-updated-at <ts>] — only the owner (anyone else gets view_owned_by_other); only the fields you give change. A stale --expected-updated-at is revision_conflict with the current view. There is no delete: --archive hides it, --restore brings it back.",
7
+ "views favorite|unfavorite <id|name> — your own favorites (at most 20); the view and everyone else's favorites are untouched. A favorite you can no longer read is left out of the list and counted as favorites_hidden.",
8
+ "views settings [--hide <field>]... [--show <field>]... — which row fields YOUR ticket list hides (priority, labels, project, assignee, updated). Per reader: colleagues are not affected. With no flag it prints the current setting.",
9
+ "Refusals keep Tower's label: view_name_required, view_name_taken, view_not_found_or_unreadable (a private view of someone else looks exactly like a missing one), view_ref_ambiguous, view_owned_by_other, view_filters_invalid (names the key), view_limit_reached, view_favorites_full, view_field_unknown, request_id_reused, revision_conflict.",
10
+ ];
@@ -0,0 +1,6 @@
1
+ /** `cockpit ticket notifications` help (BLI-21736, P10), one line a verb, joined into the `issue` help. */
2
+ export const WORK_NOTIFY_HELP_LINES = [
3
+ "notifications [--json] — your Slack delivery switch for work-inbox items: slack_enabled, slack_enabled_at, and slack_linked (do you have a Slack account Tower knows; null with a reason when it could not check). Off by default. --json shape: { settings: { slack_enabled, slack_enabled_at, slack_linked, slack_linked_reason } }.",
4
+ "notifications set --slack on|off — turn the DM on or off for yourself. On: tickets ASSIGNED to you and comments that @mention you are sent to your Slack DM once an item is 5 minutes old (and under 24 hours), unless you read it first (then it is suppressed, reason read_before_delivery). The sweep runs every 15 minutes, so allow about 20 minutes. Only items created after you turn it on are sent. commented items are never sent. DMs only, never a channel.",
5
+ "notifications receipts [--notification <id>] [--before <cursor>] [--limit n] — your delivery receipts, newest first (20 by default, at most 100). outcome is sent (Slack accepted the post, not that you read it), suppressed (read_before_delivery, issue_unreadable) or failed (slack_identity_missing, dm_open_failed, slack_post_failed, rls_mint_unavailable, ticket_read_failed; a failed item stays in your inbox and is not retried). --json shape: { receipts: [{ id, notification_id, kind, outcome, reason, slack_ts, created_at, issue_id, identifier }], next_cursor, has_more }. Refusal labels: invalid_notification_setting, invalid_cursor, invalid_filter.",
6
+ ];
@@ -141,11 +141,11 @@ const COMMAND_SUMMARY_LINES = [
141
141
  " cockpit memory [search \"<question>\"|save|update <id>|forget <id>|profile|install|status|log <win|loss|noise> --store <bli|supermemory|both> \"<reason>\"] [--container <tag>] [--limit <n>] [--no-recent] [--include-originals|--no-originals] [--include-source|--no-source] [--custom-id <id>] [--extract] [--content-stdin] [--reason <label>] [--file <path>] [--dashboard-url <url>] [--dry-run] [--json]",
142
142
  " cockpit clean [--dry-run] [--all-committed] [--reconcile] [--dashboard-url <url>] [--json]",
143
143
  " cockpit sources check [--older-than <days>] [--dashboard-url <url>] [--json]",
144
- " cockpit docs [list|tree|read <id|slug>|create --title <t>|update <id>|delete <id|slug>] [--parent <id>|root|--clear-parent] [--query <text> [--word]] [--limit <n>] [--visibility org|private] [--file <path>|--body-stdin] [--allow-empty] [--dashboard-url <url>] [--json]",
144
+ " cockpit docs [list|tree|read <id|slug>|create --title <t>|update <id>|delete <id|slug>|trash|versions <id|slug>|restore <id>] [--parent <id>|root|--clear-parent] [--query <text> [--word]] [--limit <n>] [--visibility org|private] [--file <path>|--body-stdin] [--allow-empty] [--dashboard-url <url>] [--json]",
145
145
  " 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]",
146
146
  " 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]",
147
147
  " 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]",
148
- " cockpit cal [event <id> [--occurrence-start <iso>]|export <id> [--occurrence-start <iso>]|today|week|conflicts|calendar-change <id> --color <hex-or-provider>|view-settings|view-settings-change|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
+ " cockpit cal [event <id> [--occurrence-start <iso>]|export <id> [--occurrence-start <iso>]|today|week|conflicts|calendar-change <id> --color <hex-or-provider>|view-settings|view-settings-change|people <label>|availability --with <label|email> --from <d> --to <d>|overlays [--pin <label|email>|--remove <email>]|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]",
149
149
  " cockpit project [list] [--archived] [--dashboard-url <url>] [--json]",
150
150
  " 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]",
151
151
  ` cockpit search "<words>" [--kind <category,...>] [--limit <n>] [--dashboard-url <url>] [--json]`,
@@ -260,6 +260,7 @@ function defaultSpawn(command, args, options) {
260
260
  return spawn(command, args, {
261
261
  env: options.env,
262
262
  stdio: ["pipe", "pipe", "ignore"],
263
+ windowsHide: true,
263
264
  });
264
265
  }
265
266
  function defaultRealpath(value) {
@@ -0,0 +1,134 @@
1
+ /**
2
+ * The activity verbs of `cockpit msg` (BLI-21598): `activity`, `follow`, `unfollow` and `react`. Same doors
3
+ * the browser calls (`/api/msg/activity`, `/messages/<id>/follow`, `/messages/<id>/reactions`); `--json`
4
+ * prints the door's body unchanged so ids, rows, coverage and reasons match HTTP. Split out of `msg.ts` for the file-size ratchet.
5
+ */
6
+ import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
7
+ import { writeLine } from "./cli-io.js";
8
+ import { runMsgNotify } from "./msg-notify.js";
9
+ const TAG = "[msg cli]";
10
+ const READ_DEADLINE_MS = 30_000;
11
+ const WRITE_DEADLINE_MS = 30_000;
12
+ /** One line for a body: the first line of text, trimmed to a readable length. */
13
+ function preview(content) {
14
+ const line = (content ?? "").split("\n")[0] ?? "";
15
+ return line.length > 140 ? `${line.slice(0, 137)}...` : line;
16
+ }
17
+ function countsLine(counts) {
18
+ if (!counts)
19
+ return "Unread counts are unavailable right now.";
20
+ const part = (label, count) => `${label} ${count.unread}${count.capped ? "+" : ""}`;
21
+ return `Unread: ${part("mentions", counts.mention)}, ${part("threads", counts.reply)}.`;
22
+ }
23
+ /** `cockpit msg activity [--mentions|--threads] [--unread] [--before <cursor>] [--limit <n>] [--mark-read]` */
24
+ export async function runMsgActivity(command, door) {
25
+ const query = new URLSearchParams();
26
+ if (command.activityKind)
27
+ query.set("kind", command.activityKind);
28
+ if (command.unreadOnly)
29
+ query.set("unread", "1");
30
+ if (command.before)
31
+ query.set("before", command.before);
32
+ if (command.limit)
33
+ query.set("limit", String(command.limit));
34
+ const answer = await askAgentDoor(door, {
35
+ path: `/api/msg/activity${query.size > 0 ? `?${query.toString()}` : ""}`,
36
+ method: "GET",
37
+ label: "msg activity",
38
+ timeoutMs: READ_DEADLINE_MS,
39
+ });
40
+ if (!answer.ok)
41
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
42
+ const body = answer.body;
43
+ const items = body.items ?? [];
44
+ writeLine(door.io.stderr, `${TAG} activity ${JSON.stringify({ returned: items.length, dropped: body.coverage?.dropped ?? 0, has_more: body.hasMore === true })}`);
45
+ // `--mark-read` is the person asking to be told these now: it marks exactly the unread items just printed.
46
+ let marked = null;
47
+ const unreadIds = items.filter((item) => item.read_at === null).map((item) => item.message_id);
48
+ if (command.markRead && unreadIds.length > 0) {
49
+ const read = await askAgentDoor(door, {
50
+ path: "/api/msg/activity/read",
51
+ method: "POST",
52
+ label: "msg activity read",
53
+ timeoutMs: WRITE_DEADLINE_MS,
54
+ body: { message_ids: unreadIds },
55
+ });
56
+ if (!read.ok)
57
+ return failAgentDoor(door, TAG, read.reason, `${read.detail} The items above were NOT marked read.`);
58
+ marked = read.body;
59
+ }
60
+ if (door.json)
61
+ return emitAgentDoor(door, { ok: true, ...body, ...(command.markRead ? { markRead: marked ?? { requested: 0, marked: 0 } } : {}) });
62
+ writeLine(door.io.stdout, countsLine(body.counts));
63
+ if (items.length === 0) {
64
+ writeLine(door.io.stdout, command.unreadOnly ? "Nothing unread." : "No activity yet.");
65
+ }
66
+ for (const item of items) {
67
+ const channel = body.channels?.[item.channel_id];
68
+ const where = channel ? (channel.is_dm ? "a direct message" : `#${channel.name ?? "unnamed"}`) : item.channel_id;
69
+ const who = (item.message.user_id ? body.displayNameById?.[item.message.user_id] : null) ?? item.message.agent_label ?? item.message.slack_author_label ?? "Someone";
70
+ writeLine(door.io.stdout, `${item.message_id} ${item.created_at} ${item.kind}${item.read_at === null ? " (unread)" : ""} ${where} ${who} ${preview(item.message.content)}`);
71
+ }
72
+ if ((body.coverage?.dropped ?? 0) > 0)
73
+ writeLine(door.io.stdout, `${body.coverage?.dropped} item(s) no longer readable were left out.`);
74
+ if (body.nextCursor)
75
+ writeLine(door.io.stdout, `More: --before ${body.nextCursor}`);
76
+ if (marked)
77
+ writeLine(door.io.stdout, `Marked ${marked.marked} read.`);
78
+ return 0;
79
+ }
80
+ /** `cockpit msg follow|unfollow <message-id>` — the id is the thread's top-level message. */
81
+ export async function runMsgFollow(command, door) {
82
+ const following = command.action === "follow";
83
+ const answer = await askAgentDoor(door, {
84
+ path: `/api/msg/messages/${encodeURIComponent(command.targetId ?? "")}/follow`,
85
+ method: following ? "PUT" : "DELETE",
86
+ label: `msg ${command.action}`,
87
+ timeoutMs: WRITE_DEADLINE_MS,
88
+ });
89
+ if (!answer.ok)
90
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
91
+ const follow = answer.body.follow ?? {};
92
+ writeLine(door.io.stderr, `${TAG} ${command.action} ${JSON.stringify({ message_id: command.targetId, following: follow.following === true, changed: follow.changed === true })}`);
93
+ if (door.json)
94
+ return emitAgentDoor(door, { ok: true, ...answer.body });
95
+ const state = follow.following ? "Following" : "Not following";
96
+ writeLine(door.io.stdout, `${state} thread ${command.targetId}${follow.reason ? ` (${follow.reason})` : ""}${follow.changed === false ? ": it was already that way." : "."}`);
97
+ return 0;
98
+ }
99
+ /** `cockpit msg react <message-id> <emoji> [--remove]` */
100
+ export async function runMsgReact(command, door) {
101
+ const emoji = command.emoji ?? "";
102
+ const id = encodeURIComponent(command.targetId ?? "");
103
+ const answer = command.remove
104
+ ? await askAgentDoor(door, {
105
+ path: `/api/msg/messages/${id}/reactions?emoji=${encodeURIComponent(emoji)}`,
106
+ method: "DELETE",
107
+ label: "msg react remove",
108
+ timeoutMs: WRITE_DEADLINE_MS,
109
+ })
110
+ : await askAgentDoor(door, {
111
+ path: `/api/msg/messages/${id}/reactions`,
112
+ method: "POST",
113
+ label: "msg react",
114
+ timeoutMs: WRITE_DEADLINE_MS,
115
+ body: { emoji },
116
+ });
117
+ if (!answer.ok)
118
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
119
+ writeLine(door.io.stderr, `${TAG} react ${JSON.stringify({ message_id: command.targetId, removed: command.remove === true })}`);
120
+ if (door.json)
121
+ return emitAgentDoor(door, { ok: true, removed: command.remove === true, ...(answer.body ?? {}) });
122
+ writeLine(door.io.stdout, command.remove ? `Removed ${emoji} from ${command.targetId}.` : `Reacted ${emoji} on ${command.targetId}.`);
123
+ return 0;
124
+ }
125
+ /** The one entry `runMsg` calls for activity, follow, unfollow, react and notify (BLI-21605; `resolve` turns a channel name into its id). */
126
+ export function runMsgActivityVerb(command, door, resolve) {
127
+ if (command.action === "notify")
128
+ return runMsgNotify(command, door, resolve);
129
+ if (command.action === "activity")
130
+ return runMsgActivity(command, door);
131
+ if (command.action === "react")
132
+ return runMsgReact(command, door);
133
+ return runMsgFollow(command, door);
134
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Arguments of `cockpit msg channel|members|hide|unhide` (BLI-21685): `channel <channel>` shows details, with
3
+ * `--rename <name> | --describe <text> | --archive | --unarchive` it changes them; `members <channel>` lists, with
4
+ * `--add <email> | --remove <email>` it changes membership; `hide <channel>` / `unhide <channel>` change only your own
5
+ * sidebar. Split out of `local-args-tower-msg.ts`.
6
+ *
7
+ * `--remove` is a switch for `msg react`, and an email here. `members` is spotted in the raw words and its `--remove` is
8
+ * renamed before the shared parser sees it (`msgChannelNormalizeArgs`).
9
+ */
10
+ import { optionalNonEmpty, optionalUrl } from "./local-arg-values.js";
11
+ export const MSG_CHANNEL_ACTIONS = ["channel", "members", "hide", "unhide"];
12
+ export const MSG_CHANNEL_VALUE_FLAGS = ["--rename", "--describe", "--add", "--remove-member"];
13
+ export const MSG_CHANNEL_BOOLEAN_FLAGS = ["--archive", "--unarchive"];
14
+ const MSG_CHANNEL_ONLY_FLAGS = [...MSG_CHANNEL_VALUE_FLAGS, ...MSG_CHANNEL_BOOLEAN_FLAGS];
15
+ export function isMsgChannelAction(action) {
16
+ return MSG_CHANNEL_ACTIONS.includes(action);
17
+ }
18
+ /** For `msg members ... --remove <email>`: the shared parser sees `--remove-member`, so `msg react --remove` is untouched. */
19
+ export function msgChannelNormalizeArgs(args) {
20
+ const action = args.find((arg) => !arg.startsWith("--") && (isMsgChannelAction(arg) || ["channels", "read", "send", "react", "thread", "create", "dm", "show"].includes(arg)));
21
+ if (action !== "members")
22
+ return args;
23
+ return args.map((arg) => (arg === "--remove" ? "--remove-member" : arg.startsWith("--remove=") ? `--remove-member=${arg.slice(9)}` : arg));
24
+ }
25
+ /** A flag of these verbs used on another msg verb is named, not ignored. */
26
+ export function rejectMsgChannelFlags(values) {
27
+ const stray = MSG_CHANNEL_ONLY_FLAGS.find((flag) => values.flags.has(flag) || values.booleans.has(flag));
28
+ if (stray)
29
+ throw new Error(`${stray === "--remove-member" ? "--remove" : stray} does not belong here; it is for \`cockpit msg channel\` or \`cockpit msg members\`.`);
30
+ }
31
+ function emailOf(raw, flag) {
32
+ const email = optionalNonEmpty(raw) ?? "";
33
+ if (!email.includes("@"))
34
+ throw new Error(`${flag} takes an email address; "${email}" is not one.`);
35
+ return email;
36
+ }
37
+ export function parseMsgChannelArgs(action, values, rest) {
38
+ const has = (flag) => values.flags.has(flag) || values.booleans.has(flag);
39
+ const stray = ["--thread", "--channel", "--members", "--description", "--from", "--request-id", "--expected-revision", "--name", "--out", "--file", "--mark-read", "--mentions", "--threads", "--unread", "--private", "--before", "--limit", "--level", "--timezone", "--message", "--mute", "--unmute", "--clear", "--off"].find(has);
40
+ if (stray)
41
+ throw new Error(`${stray} does not belong to \`cockpit msg ${action}\`.`);
42
+ if (values.booleans.has("--remove"))
43
+ throw new Error("--remove belongs to `cockpit msg react`; for members use `cockpit msg members <channel> --remove <email>`.");
44
+ const channelRef = optionalNonEmpty(rest[0]);
45
+ if (!channelRef)
46
+ throw new Error(`msg ${action} needs a channel id or name, e.g. \`cockpit msg ${action} general\`.`);
47
+ if (rest.length > 1)
48
+ throw new Error(`msg ${action} takes one channel, not ${rest.length}.`);
49
+ const rename = values.flags.get("--rename");
50
+ const describe = values.flags.get("--describe");
51
+ const archive = values.booleans.has("--archive");
52
+ const unarchive = values.booleans.has("--unarchive");
53
+ const add = values.flags.get("--add");
54
+ const remove = values.flags.get("--remove-member");
55
+ const base = {
56
+ kind: "msg",
57
+ action,
58
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
59
+ dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
60
+ isPrivate: false,
61
+ channelRef,
62
+ json: values.booleans.has("--json"),
63
+ };
64
+ if (action === "channel") {
65
+ if (add || remove)
66
+ throw new Error("--add and --remove belong to `cockpit msg members`.");
67
+ if (archive && unarchive)
68
+ throw new Error("Pick one of --archive or --unarchive.");
69
+ const changes = [];
70
+ if (rename !== undefined)
71
+ changes.push({ kind: "rename", name: rename.replace(/^#/, "") });
72
+ if (describe !== undefined)
73
+ changes.push({ kind: "describe", text: describe });
74
+ if (archive)
75
+ changes.push({ kind: "archive" });
76
+ if (unarchive)
77
+ changes.push({ kind: "unarchive" });
78
+ return { ...base, channelChanges: changes };
79
+ }
80
+ if (rename !== undefined || describe !== undefined || archive || unarchive) {
81
+ throw new Error("--rename, --describe, --archive and --unarchive belong to `cockpit msg channel`.");
82
+ }
83
+ if (action === "members") {
84
+ if (add && remove)
85
+ throw new Error("Pick one of --add or --remove.");
86
+ const memberChange = add ? { kind: "add", email: emailOf(add, "--add") } : remove ? { kind: "remove", email: emailOf(remove, "--remove") } : undefined;
87
+ return { ...base, ...(memberChange ? { memberChange } : {}) };
88
+ }
89
+ if (add || remove)
90
+ throw new Error(`--add and --remove belong to \`cockpit msg members\`, not \`msg ${action}\`.`);
91
+ return base;
92
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * `cockpit msg channel|members|hide|unhide` (BLI-21685): a channel's details and its changes, its people, and your own
3
+ * sidebar. Same doors the browser calls (`/api/msg/channels/<id>`, `.../members`, `.../hidden`); `--json` prints the
4
+ * door's body unchanged so ids, audience and the manage answer match HTTP. A refusal keeps the door's reason:
5
+ * channel_manage_refused, channel_name_taken, dm_not_editable, hide_not_public, person_not_found.
6
+ */
7
+ import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
8
+ import { writeLine } from "./cli-io.js";
9
+ const TAG = "[msg cli]";
10
+ const DEADLINE_MS = 30_000;
11
+ function audienceLine(details) {
12
+ if (details.audience === "org")
13
+ return "Audience: everyone in the org can read it";
14
+ if (details.audience === "participants")
15
+ return `Audience: ${details.member_count ?? 0} people in this conversation`;
16
+ return `Audience: ${details.member_count ?? 0} members`;
17
+ }
18
+ function printDetails(door, details) {
19
+ const out = door.io.stdout;
20
+ writeLine(out, `${details.is_dm ? "Direct message" : `#${details.name ?? ""}`} (${details.id})`);
21
+ writeLine(out, `Kind: ${details.is_dm ? "direct message" : details.is_private ? "private channel" : "public channel"}${details.archived_at ? " (archived)" : ""}`);
22
+ writeLine(out, `Description: ${details.description ?? "none"}`);
23
+ writeLine(out, audienceLine(details));
24
+ if (details.source_workspace)
25
+ writeLine(out, `Imported from the ${details.source_workspace} Slack workspace (a label; it grants no access)`);
26
+ writeLine(out, `You can manage it: ${details.can_manage ? "yes" : "no"}`);
27
+ if (!details.is_dm && !details.is_private)
28
+ writeLine(out, `Hidden from your sidebar: ${details.hidden ? "yes" : "no"}`);
29
+ }
30
+ async function resolveOrFail(command, door, resolve) {
31
+ const ref = command.channelRef ?? "";
32
+ const resolved = await resolve(ref);
33
+ if (resolved.status === "list_failed")
34
+ return failAgentDoor(door, TAG, resolved.reason, resolved.detail);
35
+ if (resolved.status === "not_found") {
36
+ return failAgentDoor(door, TAG, "channel_not_found_or_unreadable", `That channel does not exist, or you cannot read it: ${ref}`);
37
+ }
38
+ return resolved.id;
39
+ }
40
+ async function channelVerb(command, door, id) {
41
+ const path = `/api/msg/channels/${encodeURIComponent(id)}`;
42
+ const changes = command.channelChanges ?? [];
43
+ if (changes.length === 0) {
44
+ const answer = await askAgentDoor(door, { path, method: "GET", label: "msg channel", timeoutMs: DEADLINE_MS });
45
+ if (!answer.ok)
46
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
47
+ const details = answer.body.channel;
48
+ writeLine(door.io.stderr, `${TAG} channel details ${JSON.stringify({ channel_id: id, audience: details.audience, can_manage: details.can_manage })}`);
49
+ if (door.json)
50
+ return emitAgentDoor(door, { ok: true, ...answer.body });
51
+ printDetails(door, details);
52
+ return 0;
53
+ }
54
+ const body = {};
55
+ for (const change of changes) {
56
+ if (change.kind === "rename")
57
+ body["name"] = change.name;
58
+ if (change.kind === "describe")
59
+ body["description"] = change.text;
60
+ if (change.kind === "archive")
61
+ body["archived"] = true;
62
+ if (change.kind === "unarchive")
63
+ body["archived"] = false;
64
+ }
65
+ const answer = await askAgentDoor(door, { path, method: "PATCH", label: "msg channel update", timeoutMs: DEADLINE_MS, body });
66
+ if (!answer.ok)
67
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
68
+ writeLine(door.io.stderr, `${TAG} channel updated ${JSON.stringify({ channel_id: id, fields: Object.keys(body) })}`);
69
+ if (door.json)
70
+ return emitAgentDoor(door, { ok: true, ...answer.body });
71
+ printDetails(door, answer.body.channel);
72
+ return 0;
73
+ }
74
+ async function membersVerb(command, door, id) {
75
+ const path = `/api/msg/channels/${encodeURIComponent(id)}/members`;
76
+ const change = command.memberChange;
77
+ if (change) {
78
+ const answer = change.kind === "add"
79
+ ? await askAgentDoor(door, { path, method: "POST", label: "msg members add", timeoutMs: DEADLINE_MS, body: { email: change.email } })
80
+ : await askAgentDoor(door, { path: `${path}?email=${encodeURIComponent(change.email)}`, method: "DELETE", label: "msg members remove", timeoutMs: DEADLINE_MS });
81
+ if (!answer.ok)
82
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
83
+ writeLine(door.io.stderr, `${TAG} members ${change.kind} ${JSON.stringify({ channel_id: id })}`);
84
+ if (door.json)
85
+ return emitAgentDoor(door, { ok: true, channel_id: id, change: change.kind, email: change.email });
86
+ writeLine(door.io.stdout, change.kind === "add" ? `Added ${change.email}.` : `Removed ${change.email}. Their next read of a private channel is refused.`);
87
+ return 0;
88
+ }
89
+ const answer = await askAgentDoor(door, { path, method: "GET", label: "msg members", timeoutMs: DEADLINE_MS });
90
+ if (!answer.ok)
91
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
92
+ const body = answer.body;
93
+ const members = body.members ?? [];
94
+ writeLine(door.io.stderr, `${TAG} members listed ${JSON.stringify({ channel_id: id, count: members.length, audience: body.audience ?? null })}`);
95
+ if (door.json)
96
+ return emitAgentDoor(door, { ok: true, ...body });
97
+ if (body.audience === "org")
98
+ writeLine(door.io.stdout, "Everyone in the org can read this channel.");
99
+ for (const member of members)
100
+ writeLine(door.io.stdout, `${member.user_id} ${member.role.padEnd(14)} ${body.display_names?.[member.user_id] ?? "someone"}`);
101
+ if (members.length === 0 && body.audience !== "org")
102
+ writeLine(door.io.stdout, "No members.");
103
+ return 0;
104
+ }
105
+ async function hideVerb(command, door, id) {
106
+ const hide = command.action === "hide";
107
+ const answer = await askAgentDoor(door, { path: `/api/msg/channels/${encodeURIComponent(id)}/hidden`, method: hide ? "PUT" : "DELETE", label: hide ? "msg hide" : "msg unhide", timeoutMs: DEADLINE_MS });
108
+ if (!answer.ok)
109
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
110
+ writeLine(door.io.stderr, `${TAG} ${command.action} ${JSON.stringify({ channel_id: id })}`);
111
+ if (door.json)
112
+ return emitAgentDoor(door, { ok: true, ...answer.body });
113
+ writeLine(door.io.stdout, hide ? "Hidden from your sidebar. Everyone can still read, search and link to it." : "Shown in your sidebar again.");
114
+ return 0;
115
+ }
116
+ export async function runMsgChannel(command, door, resolve) {
117
+ const id = await resolveOrFail(command, door, resolve);
118
+ if (typeof id === "number")
119
+ return id;
120
+ if (command.action === "channel")
121
+ return channelVerb(command, door, id);
122
+ if (command.action === "members")
123
+ return membersVerb(command, door, id);
124
+ return hideVerb(command, door, id);
125
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Arguments of `cockpit msg context <message-id> [--add <kind:id>] [--remove <link-id>] [--json]` (BLI-21829).
3
+ * `--add doc:<id>|ticket:<BLI-id or uuid>|event:<id>[@<iso occurrence>]|note:<id>`; `--remove` takes the message id first and the
4
+ * link id after it. Split out of `local-args-tower-msg.ts`.
5
+ */
6
+ import { optionalNonEmpty, optionalUrl } from "./local-arg-values.js";
7
+ export const MSG_CONTEXT_VALUE_FLAGS = ["--add"];
8
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
9
+ const IDENTIFIER = /^[A-Za-z][A-Za-z0-9]{1,12}-\d+$/;
10
+ const KINDS = ["doc", "ticket", "event", "note"];
11
+ /** `--add` used on another msg verb is named, not ignored. */
12
+ export function rejectMsgContextFlags(values) {
13
+ if (values.flags.has("--add"))
14
+ throw new Error("--add belongs to `cockpit msg context`.");
15
+ }
16
+ export function parseContextSpec(spec) {
17
+ const match = /^(doc|ticket|event|note):(.+)$/i.exec(spec.trim());
18
+ if (!match)
19
+ throw new Error("--add takes doc:<id>, ticket:<BLI-id or uuid>, event:<id>[@<iso occurrence>] or note:<id>.");
20
+ const kind = match[1].toLowerCase();
21
+ const [id = "", occurrence] = match[2].split("@", 2);
22
+ const ok = kind === "ticket" ? UUID.test(id) || IDENTIFIER.test(id) : UUID.test(id);
23
+ if (!ok)
24
+ throw new Error(`--add ${kind}: needs ${kind === "ticket" ? "a BLI id or a uuid" : "a uuid"}, not "${id}".`);
25
+ if (occurrence !== undefined) {
26
+ if (kind !== "event")
27
+ throw new Error("An @<iso occurrence> belongs to event: only.");
28
+ const time = Date.parse(occurrence);
29
+ if (Number.isNaN(time))
30
+ throw new Error(`--add event: the occurrence "${occurrence}" is not an ISO time.`);
31
+ return { kind, id, occurrence: new Date(time).toISOString() };
32
+ }
33
+ return { kind, id };
34
+ }
35
+ export function parseMsgContextArgs(values, rest) {
36
+ const stray = ["--channel", "--members", "--description", "--from", "--expected-revision", "--name", "--out", "--file", "--level", "--timezone", "--message", "--to", "--before", "--limit", "--thread", "--request-id"]
37
+ .find((flag) => values.flags.has(flag));
38
+ if (stray || values.booleans.has("--private"))
39
+ throw new Error(`${stray ?? "--private"} does not belong to \`cockpit msg context\`.`);
40
+ const targetId = optionalNonEmpty(rest[0]);
41
+ if (!targetId || !UUID.test(targetId)) {
42
+ throw new Error("msg context needs a message id (a UUID), e.g. `cockpit msg context <message-id> --json`.");
43
+ }
44
+ const add = optionalNonEmpty(values.flags.get("--add"));
45
+ const removing = values.booleans.has("--remove");
46
+ if (add && removing)
47
+ throw new Error("msg context takes --add or --remove, not both.");
48
+ const base = {
49
+ kind: "msg",
50
+ action: "context",
51
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
52
+ dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
53
+ isPrivate: false,
54
+ json: values.booleans.has("--json"),
55
+ targetId,
56
+ };
57
+ if (removing) {
58
+ const linkId = optionalNonEmpty(rest[1]);
59
+ if (!linkId || !UUID.test(linkId) || rest.length > 2) {
60
+ throw new Error("msg context --remove takes the message id and then the link id, e.g. `cockpit msg context <message-id> --remove <link-id>`.");
61
+ }
62
+ return { ...base, contextRemove: linkId };
63
+ }
64
+ if (rest.length > 1)
65
+ throw new Error(`msg context takes one message id, not ${rest.length}.`);
66
+ return { ...base, ...(add ? { contextAdd: parseContextSpec(add) } : {}) };
67
+ }