@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
@@ -0,0 +1,93 @@
1
+ /**
2
+ * `cockpit docs inbox|subscribe|unsubscribe|notifications` argument parsing (BLI-21741, docs parity step 8).
3
+ *
4
+ * `inbox [--unread] [--kind mentioned|commented|edited] [--before <cursor>] [--limit <n>]` reads the inbox.
5
+ * `inbox read <id>... | --all` and `inbox unread <id>...` mark items. `subscribe|unsubscribe <id|slug>` follow or
6
+ * stop following a page. `notifications [--mentions on|off] [--comments on|off] [--edits on|off]` reads the
7
+ * three docs notification kinds, or sets the ones named. The door checks authority; this file only checks shape.
8
+ */
9
+ import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
10
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
11
+ const KINDS = ["mentioned", "commented", "edited"];
12
+ const NOTIFY_FLAGS = ["--mentions", "--comments", "--edits"];
13
+ const VALUE_FLAGS = ["--home", "--dashboard-url", "--kind", "--before", "--limit", ...NOTIFY_FLAGS];
14
+ function onOff(flag, raw) {
15
+ if (raw === undefined)
16
+ return undefined;
17
+ if (raw === "on")
18
+ return true;
19
+ if (raw === "off")
20
+ return false;
21
+ throw new Error(`${flag} takes on or off, not "${raw}".`);
22
+ }
23
+ export function parseDocsInboxArgs(args) {
24
+ const values = parseNamedArgs(args, { allowedFlags: [...VALUE_FLAGS, "--unread", "--all", "--json"], valueFlags: VALUE_FLAGS });
25
+ const verb = args[0];
26
+ const rest = values.positionals.slice(1);
27
+ const base = {
28
+ kind: "docs",
29
+ clearParent: false,
30
+ bodyStdin: false,
31
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
32
+ dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
33
+ json: values.booleans.has("--json"),
34
+ };
35
+ const stray = (allowed) => {
36
+ const bad = [...values.flags.keys(), ...values.booleans].find((flag) => !allowed.includes(flag) && flag !== "--json" && flag !== "--home" && flag !== "--dashboard-url");
37
+ if (bad)
38
+ throw new Error(`${bad} does not belong to \`cockpit docs ${verb === "inbox" && rest[0] ? `inbox ${rest[0]}` : verb}\`.`);
39
+ };
40
+ if (verb === "subscribe" || verb === "unsubscribe") {
41
+ stray([]);
42
+ if (rest.length !== 1)
43
+ throw new Error(`docs ${verb} needs exactly one document id or slug.`);
44
+ return { ...base, action: verb, docRef: rest[0] };
45
+ }
46
+ if (verb === "notifications") {
47
+ stray([...NOTIFY_FLAGS]);
48
+ if (rest.length > 0)
49
+ throw new Error("docs notifications takes flags only: --mentions, --comments, --edits (on or off).");
50
+ const set = {
51
+ mentions: onOff("--mentions", values.flags.get("--mentions")),
52
+ comments: onOff("--comments", values.flags.get("--comments")),
53
+ edits: onOff("--edits", values.flags.get("--edits")),
54
+ };
55
+ const given = Object.fromEntries(Object.entries(set).filter(([, value]) => value !== undefined));
56
+ return { ...base, action: "notifications", ...(Object.keys(given).length > 0 ? { notifySet: given } : {}) };
57
+ }
58
+ // inbox
59
+ if (rest[0] === "read" || rest[0] === "unread") {
60
+ const marking = rest[0];
61
+ stray(["--all"]);
62
+ const ids = rest.slice(1);
63
+ const all = values.booleans.has("--all");
64
+ if (all && marking === "unread")
65
+ throw new Error("--all only marks items read. Name the ids to mark unread.");
66
+ if (all && ids.length > 0)
67
+ throw new Error("docs inbox read takes ids or --all, not both.");
68
+ if (!all && ids.length === 0)
69
+ throw new Error(`docs inbox ${marking} needs one or more item ids (from \`cockpit docs inbox --json\`)${marking === "read" ? ", or --all" : ""}.`);
70
+ const bad = ids.find((id) => !UUID.test(id));
71
+ if (bad)
72
+ throw new Error(`"${bad}" is not an item id (a UUID); take it from \`cockpit docs inbox --json\`.`);
73
+ return { ...base, action: "inbox-read", inboxRead: marking === "read", ...(all ? { inboxAll: true } : { inboxIds: ids }) };
74
+ }
75
+ stray(["--unread", "--kind", "--before", "--limit"]);
76
+ if (rest.length > 0)
77
+ throw new Error(`docs inbox does not know "${rest[0]}". Use \`inbox read <id>...\`, \`inbox unread <id>...\`, or flags.`);
78
+ const kind = optionalNonEmpty(values.flags.get("--kind"));
79
+ if (kind !== undefined && !KINDS.includes(kind))
80
+ throw new Error(`--kind takes ${KINDS.join(", ")}, not "${kind}".`);
81
+ const before = optionalNonEmpty(values.flags.get("--before"));
82
+ const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
83
+ if (limit !== undefined && limit > 100)
84
+ throw new Error("--limit takes a whole number from 1 to 100.");
85
+ return {
86
+ ...base,
87
+ action: "inbox",
88
+ ...(values.booleans.has("--unread") ? { inboxUnread: true } : {}),
89
+ ...(kind ? { inboxKind: kind } : {}),
90
+ ...(before ? { inboxBefore: before } : {}),
91
+ ...(limit !== undefined ? { limit } : {}),
92
+ };
93
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * `cockpit docs inbox|subscribe|unsubscribe|notifications` — the docs inbox from a terminal (BLI-21741).
3
+ * A sibling of `docs.ts`, like `docs-comments.ts`.
4
+ *
5
+ * `inbox [--unread] [--kind k] [--before c] [--limit n]` → `GET /api/docs/inbox`
6
+ * `inbox read <id>... | --all`, `inbox unread <id>...` → `POST /api/docs/inbox/read`
7
+ * `subscribe <id|slug>` → `PUT /api/docs/documents/[id]/follow`
8
+ * `unsubscribe <id|slug>` → `DELETE /api/docs/documents/[id]/follow`
9
+ * `notifications [--mentions on|off] ...` → `GET|PUT /api/docs/notifications/settings`
10
+ *
11
+ * The door's `reason` labels (`inbox_ids_invalid`, `inbox_too_many_ids`, `invalid_notification_setting`,
12
+ * `document_not_found_or_unreadable`, ...) come back verbatim, and the mention policy sentence is printed as the door says it.
13
+ */
14
+ import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
15
+ import { writeLine } from "./cli-io.js";
16
+ import { resolveDocId } from "./docs.js";
17
+ const TAG = "[docs inbox cli]";
18
+ const DEADLINE_MS = 30_000;
19
+ function itemLine(item) {
20
+ const who = item.actor_name ?? "Someone";
21
+ const what = item.kind === "mentioned" ? `${who} mentioned you`
22
+ : item.kind === "commented" ? (item.event_count > 1 ? `${who} and others: ${item.event_count} comments` : `${who} commented`)
23
+ : (item.event_count > 1 ? `${who} edited: ${item.event_count} edits` : `${who} edited`);
24
+ return `${item.id} ${item.read_at === null ? "unread" : "read "} ${item.title || "Untitled"} ${what} ${item.updated_at} (page ${item.document_id})`;
25
+ }
26
+ function settingsLine(settings) {
27
+ const word = (on) => (on ? "on" : "off");
28
+ return `mentions ${word(settings.mentions)}, comments ${word(settings.comments)}, edits ${word(settings.edits)}`;
29
+ }
30
+ async function readInbox(command, door) {
31
+ const params = new URLSearchParams();
32
+ if (command.inboxUnread)
33
+ params.set("unread", "true");
34
+ if (command.inboxKind)
35
+ params.set("kind", command.inboxKind);
36
+ if (command.inboxBefore)
37
+ params.set("before", command.inboxBefore);
38
+ if (command.limit !== undefined)
39
+ params.set("limit", String(command.limit));
40
+ const query = params.toString();
41
+ const answer = await askAgentDoor(door, { path: `/api/docs/inbox${query ? `?${query}` : ""}`, method: "GET", label: "docs inbox", timeoutMs: DEADLINE_MS });
42
+ if (!answer.ok)
43
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
44
+ const body = answer.body;
45
+ writeLine(door.io.stderr, `${TAG} read ${JSON.stringify({ returned: body.coverage.returned, dropped: body.coverage.dropped, unread: body.counts.unread })}`);
46
+ if (door.json)
47
+ return emitAgentDoor(door, { ok: true, ...body });
48
+ writeLine(door.io.stdout, `${body.counts.unread}${body.counts.capped ? "+" : ""} unread. Showing ${body.coverage.returned}.`);
49
+ if (body.items.length === 0)
50
+ writeLine(door.io.stdout, body.explanation ?? (command.inboxUnread ? "No unread activity." : "No activity yet."));
51
+ for (const item of body.items)
52
+ writeLine(door.io.stdout, itemLine(item));
53
+ if (body.coverage.dropped > 0)
54
+ writeLine(door.io.stdout, `${body.coverage.dropped} item(s) hidden because you can no longer read their page.`);
55
+ if (body.next_cursor)
56
+ writeLine(door.io.stdout, `More: --before ${body.next_cursor}`);
57
+ return 0;
58
+ }
59
+ async function markInbox(command, door) {
60
+ const read = command.inboxRead !== false;
61
+ const answer = await askAgentDoor(door, {
62
+ path: "/api/docs/inbox/read", method: "POST", label: read ? "docs inbox read" : "docs inbox unread", timeoutMs: DEADLINE_MS,
63
+ body: command.inboxAll ? { all: true, read: true } : { ids: command.inboxIds ?? [], read },
64
+ });
65
+ if (!answer.ok)
66
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
67
+ const body = answer.body;
68
+ writeLine(door.io.stderr, `${TAG} mark ${JSON.stringify({ requested: body.requested, updated: body.updated, skipped: body.skipped, read: body.read })}`);
69
+ if (door.json)
70
+ return emitAgentDoor(door, { ok: true, ...body });
71
+ writeLine(door.io.stdout, `Marked ${body.updated} item(s) ${body.read ? "read" : "unread"}${body.skipped > 0 ? ` (${body.skipped} skipped: another unread item already covers that thread or page)` : ""}. ${body.counts.unread} unread.`);
72
+ return 0;
73
+ }
74
+ async function followDoc(command, door) {
75
+ const ref = command.docRef ?? "";
76
+ const resolved = await resolveDocId(door, ref);
77
+ if (resolved.status === "list_failed")
78
+ return failAgentDoor(door, TAG, resolved.reason, resolved.detail);
79
+ if (resolved.status === "not_found") {
80
+ return failAgentDoor(door, TAG, "document_not_found_or_unreadable", `That document does not exist, or you cannot read it: ${ref}`);
81
+ }
82
+ const follow = command.action === "subscribe";
83
+ const label = follow ? "docs subscribe" : "docs unsubscribe";
84
+ const answer = await askAgentDoor(door, {
85
+ path: `/api/docs/documents/${encodeURIComponent(resolved.id)}/follow`, method: follow ? "PUT" : "DELETE", label, timeoutMs: DEADLINE_MS,
86
+ });
87
+ if (!answer.ok)
88
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
89
+ const body = answer.body;
90
+ writeLine(door.io.stderr, `${TAG} ${label} ${JSON.stringify({ document_id: body.document_id, following: body.following, reason: body.reason })}`);
91
+ if (door.json)
92
+ return emitAgentDoor(door, { ok: true, ...body });
93
+ writeLine(door.io.stdout, `${body.following ? "Following" : "Not following"} ${body.document_id} (${body.reason}).`);
94
+ writeLine(door.io.stdout, body.mention_policy);
95
+ return 0;
96
+ }
97
+ async function notifications(command, door) {
98
+ const set = command.notifySet;
99
+ const answer = await askAgentDoor(door, {
100
+ path: "/api/docs/notifications/settings", method: set ? "PUT" : "GET", label: set ? "docs notifications set" : "docs notifications", timeoutMs: DEADLINE_MS,
101
+ ...(set ? { body: set } : {}),
102
+ });
103
+ if (!answer.ok)
104
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
105
+ const body = answer.body;
106
+ writeLine(door.io.stderr, `${TAG} notifications ${JSON.stringify({ set: Boolean(set), ...body.settings })}`);
107
+ if (door.json)
108
+ return emitAgentDoor(door, { ok: true, ...body });
109
+ writeLine(door.io.stdout, settingsLine(body.settings));
110
+ if (body.explanation)
111
+ writeLine(door.io.stdout, body.explanation);
112
+ return 0;
113
+ }
114
+ export async function runDocsInbox(command, door) {
115
+ switch (command.action) {
116
+ case "inbox":
117
+ return readInbox(command, door);
118
+ case "inbox-read":
119
+ return markInbox(command, door);
120
+ case "subscribe":
121
+ case "unsubscribe":
122
+ return followDoc(command, door);
123
+ default:
124
+ return notifications(command, door);
125
+ }
126
+ }
@@ -16,6 +16,7 @@
16
16
  import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
17
17
  import { writeLine } from "./cli-io.js";
18
18
  import { resolveDocId } from "./docs.js";
19
+ import { printSyncDetail } from "./docs-sync.js";
19
20
  const TAG = "[docs cli]";
20
21
  const READ_DEADLINE_MS = 30_000;
21
22
  const WRITE_DEADLINE_MS = 60_000;
@@ -45,6 +46,10 @@ export async function syncStatusDocs(command, door) {
45
46
  if (!answer.ok)
46
47
  return failAgentDoor(door, TAG, answer.reason, answer.detail);
47
48
  const body = answer.body;
49
+ // BLI-21875: one page also answers with its receipts, losses and conflicts; those lines are the door's own sentences.
50
+ if (!door.json && command.docRef && answer.body.detail) {
51
+ return printSyncDetail(door, command.docRef, answer.body);
52
+ }
48
53
  if (door.json) {
49
54
  return emitAgentDoor(door, {
50
55
  ...answer.body,
@@ -93,7 +93,8 @@ async function fetchOneDocument(door, ref, id) {
93
93
  }
94
94
  function writeDocumentBlock(io, document) {
95
95
  writeLine(io.stdout, String(document["title"] ?? ""));
96
- writeLine(io.stdout, `${document["slug"] ?? "(no slug)"} · ${document["visibility"] ?? ""} · ${document["source"] ?? ""}`);
96
+ const revision = typeof document["revision"] === "number" ? ` · revision ${document["revision"]}` : "";
97
+ writeLine(io.stdout, `${document["slug"] ?? "(no slug)"} · ${document["visibility"] ?? ""} · ${document["source"] ?? ""}${revision}`);
97
98
  writeLine(io.stdout, "");
98
99
  writeLine(io.stdout, String(document["body_markdown"] ?? ""));
99
100
  }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * `cockpit docs trash`, `docs restore` and `docs versions` — recovery from a terminal (BLI-21413).
3
+ * A sibling of `docs.ts`, like `docs-share.ts`.
4
+ *
5
+ * `trash [--cursor C] [--limit N]` → `GET /api/docs/trash`
6
+ * `restore <id>` → `POST /api/docs/documents/[id]/restore`
7
+ * `versions <id|slug> [--before N] [--limit N]` → `GET /api/docs/documents/[id]/versions`
8
+ * `versions <id|slug> --version V` → `GET /api/docs/documents/[id]/versions/[V]` (one body)
9
+ * `restore <id|slug> --version V [--expected-revision N]` → `POST .../versions/[V]/restore`
10
+ *
11
+ * Every refusal keeps the door's own reason label (`not_restore_manager`, `not_in_trash`,
12
+ * `revision_conflict`, `version_not_found`, ...). A restore that had to change the page's address says
13
+ * so in plain words.
14
+ */
15
+ import { randomUUID } from "node:crypto";
16
+ import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
17
+ import { writeLine } from "./cli-io.js";
18
+ import { resolveDocId } from "./docs.js";
19
+ const TAG = "[docs cli]";
20
+ const READ_DEADLINE_MS = 30_000;
21
+ const WRITE_DEADLINE_MS = 60_000;
22
+ export async function trashDocs(command, door) {
23
+ const params = new URLSearchParams();
24
+ if (command.limit !== undefined)
25
+ params.set("limit", String(command.limit));
26
+ if (command.cursor)
27
+ params.set("cursor", command.cursor);
28
+ const search = params.toString();
29
+ const answer = await askAgentDoor(door, {
30
+ path: search ? `/api/docs/trash?${search}` : "/api/docs/trash",
31
+ method: "GET",
32
+ label: "docs trash",
33
+ timeoutMs: READ_DEADLINE_MS,
34
+ });
35
+ if (!answer.ok)
36
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
37
+ const body = answer.body;
38
+ const documents = body.documents ?? [];
39
+ writeLine(door.io.stderr, `${TAG} trash ${JSON.stringify({ returned: documents.length, complete: body.complete === true })}`);
40
+ if (door.json)
41
+ return emitAgentDoor(door, { ok: true, ...body, documents });
42
+ if (documents.length === 0) {
43
+ writeLine(door.io.stdout, "Nothing to restore: the trash holds no page you may restore.");
44
+ return 0;
45
+ }
46
+ for (const doc of documents) {
47
+ writeLine(door.io.stdout, `${doc.id} ${doc.deleted_at ?? "?"} ${doc.slug ?? "(no slug)"} ${doc.title}`);
48
+ }
49
+ writeLine(door.io.stdout, "");
50
+ writeLine(door.io.stdout, `${documents.length} deleted page(s). Restore one with \`cockpit docs restore <id>\`.`);
51
+ if (body.next_cursor)
52
+ writeLine(door.io.stdout, `More: cockpit docs trash --cursor ${body.next_cursor}`);
53
+ return 0;
54
+ }
55
+ export async function versionsDocs(command, door) {
56
+ const ref = command.docRef ?? "";
57
+ const resolved = await resolveDocId(door, ref);
58
+ if (resolved.status === "list_failed")
59
+ return failAgentDoor(door, TAG, resolved.reason, resolved.detail);
60
+ if (resolved.status === "not_found") {
61
+ return failAgentDoor(door, TAG, "document_not_found_or_unreadable", `That document does not exist, or you cannot read it: ${ref}`);
62
+ }
63
+ const base = `/api/docs/documents/${encodeURIComponent(resolved.id)}/versions`;
64
+ if (command.versionId) {
65
+ const one = await askAgentDoor(door, {
66
+ path: `${base}/${encodeURIComponent(command.versionId)}`,
67
+ method: "GET",
68
+ label: "docs version",
69
+ timeoutMs: READ_DEADLINE_MS,
70
+ });
71
+ if (!one.ok)
72
+ return failAgentDoor(door, TAG, one.reason, one.detail);
73
+ const version = one.body.version ?? {};
74
+ writeLine(door.io.stderr, `${TAG} version ${JSON.stringify({ document_id: resolved.id, version_id: command.versionId, chars: version.body_markdown?.length ?? 0 })}`);
75
+ if (door.json)
76
+ return emitAgentDoor(door, { ok: true, version });
77
+ writeLine(door.io.stdout, version.body_markdown ?? "");
78
+ return 0;
79
+ }
80
+ const params = new URLSearchParams();
81
+ if (command.limit !== undefined)
82
+ params.set("limit", String(command.limit));
83
+ if (command.before !== undefined)
84
+ params.set("before", String(command.before));
85
+ const search = params.toString();
86
+ const answer = await askAgentDoor(door, {
87
+ path: search ? `${base}?${search}` : base,
88
+ method: "GET",
89
+ label: "docs versions",
90
+ timeoutMs: READ_DEADLINE_MS,
91
+ });
92
+ if (!answer.ok)
93
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
94
+ const body = answer.body;
95
+ const versions = body.versions ?? [];
96
+ writeLine(door.io.stderr, `${TAG} versions ${JSON.stringify({ document_id: resolved.id, returned: versions.length, total: body.total ?? null })}`);
97
+ if (door.json)
98
+ return emitAgentDoor(door, { ok: true, document_id: resolved.id, ...body, versions });
99
+ if (versions.length === 0) {
100
+ writeLine(door.io.stdout, "No earlier version yet.");
101
+ return 0;
102
+ }
103
+ for (const version of versions) {
104
+ writeLine(door.io.stdout, `v${String(version.version_number).padEnd(5)} ${version.id} ${version.created_at}${version.origin ? ` (${version.origin})` : ""}`);
105
+ }
106
+ writeLine(door.io.stdout, "");
107
+ writeLine(door.io.stdout, `${versions.length} of ${body.total ?? "?"} versions, newest first. Read one with --version <id>; restore one with \`cockpit docs restore ${ref} --version <id>\`.`);
108
+ if (body.next_before !== null && body.next_before !== undefined)
109
+ writeLine(door.io.stdout, `Older: cockpit docs versions ${ref} --before ${body.next_before}`);
110
+ return 0;
111
+ }
112
+ /** Says where the restore stands, then fails with the door's own reason (a conflict names the current revision). */
113
+ function refuse(door, answer, requestId) {
114
+ const mutation = answer.body?.mutation;
115
+ writeLine(door.io.stderr, `${TAG} retry unchanged input with --request-id ${requestId}.`);
116
+ if (mutation?.current) {
117
+ writeLine(door.io.stderr, `${TAG} conflict ${JSON.stringify({ request_id: requestId, current_revision: mutation.current.revision ?? mutation.revision })}`);
118
+ }
119
+ return failAgentDoor(door, TAG, answer.reason, answer.detail, mutation ? { mutation } : {});
120
+ }
121
+ export async function restoreDocs(command, door) {
122
+ const requestId = command.requestId ?? randomUUID();
123
+ const ref = command.docRef ?? "";
124
+ if (!command.versionId) {
125
+ // Undelete: the page is not in the live list, so the argument is its id (the trash prints it).
126
+ writeLine(door.io.stderr, `${TAG} request ${JSON.stringify({ request_id: requestId })}`);
127
+ const answer = await askAgentDoor(door, {
128
+ path: `/api/docs/documents/${encodeURIComponent(ref)}/restore`,
129
+ method: "POST",
130
+ label: "docs restore",
131
+ timeoutMs: WRITE_DEADLINE_MS,
132
+ body: { request_id: requestId },
133
+ });
134
+ if (!answer.ok)
135
+ return refuse(door, answer, requestId);
136
+ const result = answer.body;
137
+ writeLine(door.io.stderr, `${TAG} restored ${JSON.stringify({ document_id: result.document_id ?? ref, slug_changed: result.slug_changed === true })}`);
138
+ if (door.json)
139
+ return emitAgentDoor(door, { ok: true, ...result });
140
+ writeLine(door.io.stdout, `Restored ${result.document_id ?? ref} from the trash with the same id.`);
141
+ if (result.slug_changed) {
142
+ writeLine(door.io.stdout, `Its address changed: another page now uses ${result.old_slug ?? "the old address"}, so this page is now ${result.new_slug ?? "at a new address"}.`);
143
+ }
144
+ return 0;
145
+ }
146
+ const resolved = await resolveDocId(door, ref);
147
+ if (resolved.status === "list_failed")
148
+ return failAgentDoor(door, TAG, resolved.reason, resolved.detail);
149
+ if (resolved.status === "not_found") {
150
+ return failAgentDoor(door, TAG, "document_not_found_or_unreadable", `That document does not exist, or you cannot write to it: ${ref}`);
151
+ }
152
+ writeLine(door.io.stderr, `${TAG} request ${JSON.stringify({ request_id: requestId, expected_revision: command.expectedRevision })}`);
153
+ const answer = await askAgentDoor(door, {
154
+ path: `/api/docs/documents/${encodeURIComponent(resolved.id)}/versions/${encodeURIComponent(command.versionId)}/restore`,
155
+ method: "POST",
156
+ label: "docs restore version",
157
+ timeoutMs: WRITE_DEADLINE_MS,
158
+ body: {
159
+ request_id: requestId,
160
+ ...(command.expectedRevision !== undefined ? { expected_revision: command.expectedRevision } : {}),
161
+ },
162
+ });
163
+ if (!answer.ok)
164
+ return refuse(door, answer, requestId);
165
+ const { document = {}, mutation } = answer.body;
166
+ writeLine(door.io.stderr, `${TAG} version restored ${JSON.stringify({ document_id: resolved.id, version_id: command.versionId, request_id: requestId, revision: mutation?.["revision"] ?? null })}`);
167
+ if (door.json)
168
+ return emitAgentDoor(door, { ok: true, document, mutation });
169
+ writeLine(door.io.stdout, mutation?.["reason"] === "unchanged"
170
+ ? `That version already matched the page; nothing changed (revision ${String(mutation?.["revision"] ?? "?")}).`
171
+ : `Restored the version as the current text of ${String(document["title"] ?? resolved.id)}, revision ${String(mutation?.["revision"] ?? "?")}. What was there before is kept as a version.`);
172
+ return 0;
173
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * `cockpit docs templates|refs|review|maintenance` and `docs create --template` argument parsing (BLI-21849, docs parity step 11).
3
+ *
4
+ * `templates` lists the curated templates. `create --template <id|slug> --title <t>` copies one into a new page (no body is accepted
5
+ * beside a template). `refs <id|slug>` shows links from and to a page. `review <id|slug>` shows the owner and review schedule;
6
+ * with `--owner <email|me|none>`, `--every <days>`, `--no-schedule` or `--reviewed` it changes them. `maintenance [--mine] [--state ...]`
7
+ * lists pages that are due, unowned or yours. The door checks authority; this file only checks shape.
8
+ */
9
+ import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
10
+ const VALUE_FLAGS = ["--home", "--dashboard-url", "--template", "--title", "--parent", "--visibility", "--owner", "--every", "--state"];
11
+ const BOOLEAN_FLAGS = ["--json", "--mine", "--no-schedule", "--reviewed"];
12
+ const STATES = ["review_due", "unowned", "all"];
13
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
14
+ export function isDocsRefsInvocation(args) {
15
+ return ["templates", "refs", "review", "maintenance"].includes(args[0] ?? "") || (args[0] === "create" && args.some((arg) => arg === "--template" || arg.startsWith("--template=")));
16
+ }
17
+ export function parseDocsRefsArgs(args) {
18
+ const values = parseNamedArgs(args, { allowedFlags: [...VALUE_FLAGS, ...BOOLEAN_FLAGS], valueFlags: VALUE_FLAGS });
19
+ const verb = args[0];
20
+ const rest = values.positionals.slice(1);
21
+ const base = {
22
+ kind: "docs",
23
+ clearParent: false,
24
+ bodyStdin: false,
25
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
26
+ dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
27
+ json: values.booleans.has("--json"),
28
+ };
29
+ const only = (allowed) => {
30
+ for (const flag of [...VALUE_FLAGS.slice(2), ...BOOLEAN_FLAGS.slice(1)]) {
31
+ const given = values.flags.has(flag) || values.booleans.has(flag);
32
+ if (given && !allowed.includes(flag))
33
+ throw new Error(`${flag} does not belong to \`cockpit docs ${verb}\`.`);
34
+ }
35
+ };
36
+ if (verb === "templates") {
37
+ only([]);
38
+ if (rest.length !== 0)
39
+ throw new Error("docs templates takes no arguments.");
40
+ return { ...base, action: "templates" };
41
+ }
42
+ if (verb === "create") {
43
+ only(["--template", "--title", "--parent", "--visibility"]);
44
+ if (rest.length !== 0)
45
+ throw new Error("docs create --template takes no body: the template's text is the body. Use --title for the name.");
46
+ const title = optionalNonEmpty(values.flags.get("--title"));
47
+ if (!title)
48
+ throw new Error("docs create needs --title.");
49
+ const parent = optionalNonEmpty(values.flags.get("--parent"));
50
+ if (parent && !UUID.test(parent))
51
+ throw new Error("--parent must be a document id (a UUID).");
52
+ const visibility = optionalNonEmpty(values.flags.get("--visibility"));
53
+ if (visibility && visibility !== "org" && visibility !== "private")
54
+ throw new Error("--visibility must be org or private.");
55
+ return {
56
+ ...base,
57
+ action: "create",
58
+ title,
59
+ ...(parent ? { parentId: parent } : {}),
60
+ ...(visibility ? { visibility: visibility } : {}),
61
+ docsRefs: { templateId: optionalNonEmpty(values.flags.get("--template")) },
62
+ };
63
+ }
64
+ if (verb === "maintenance") {
65
+ only(["--mine", "--state"]);
66
+ if (rest.length !== 0)
67
+ throw new Error("docs maintenance takes no page, e.g. `cockpit docs maintenance --state review_due`.");
68
+ const state = optionalNonEmpty(values.flags.get("--state")) ?? "all";
69
+ if (!STATES.includes(state))
70
+ throw new Error(`--state must be one of ${STATES.join(", ")}.`);
71
+ return { ...base, action: "maintenance", docsRefs: { state: state, mine: values.booleans.has("--mine") } };
72
+ }
73
+ if (rest.length !== 1)
74
+ throw new Error(`docs ${verb} needs exactly one page id or slug, e.g. \`cockpit docs ${verb} runbook\`.`);
75
+ if (verb === "refs") {
76
+ only([]);
77
+ return { ...base, action: "refs", docRef: rest[0] };
78
+ }
79
+ only(["--owner", "--every", "--no-schedule", "--reviewed"]);
80
+ const owner = optionalNonEmpty(values.flags.get("--owner"));
81
+ const everyRaw = optionalNonEmpty(values.flags.get("--every"));
82
+ const noSchedule = values.booleans.has("--no-schedule");
83
+ if (everyRaw !== undefined && noSchedule)
84
+ throw new Error("--every and --no-schedule say opposite things; give one.");
85
+ let everyDays;
86
+ if (everyRaw !== undefined) {
87
+ everyDays = /^\d+$/.test(everyRaw) ? Number(everyRaw) : Number.NaN;
88
+ if (!Number.isInteger(everyDays) || everyDays < 1 || everyDays > 730)
89
+ throw new Error("--every is a whole number of days from 1 to 730.");
90
+ }
91
+ else if (noSchedule) {
92
+ everyDays = null;
93
+ }
94
+ return {
95
+ ...base,
96
+ action: "review",
97
+ docRef: rest[0],
98
+ docsRefs: {
99
+ ...(owner !== undefined ? { owner: owner.toLowerCase() === "none" ? null : owner } : {}),
100
+ ...(everyDays !== undefined ? { everyDays } : {}),
101
+ ...(values.booleans.has("--reviewed") ? { reviewed: true } : {}),
102
+ },
103
+ };
104
+ }