@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,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
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * `cockpit docs templates|create --template|refs|review|maintenance` (BLI-21849): templates, links between pages and
3
+ * page owners with review dates, over the same doors the browser calls.
4
+ *
5
+ * templates GET /api/docs/templates
6
+ * create --template <t> --title <x> POST /api/docs/documents { template_id }
7
+ * refs <page> GET /api/docs/documents/[id]/references
8
+ * review <page> [--owner --every --no-schedule --reviewed] GET or PUT /api/docs/documents/[id]/stewardship
9
+ * maintenance [--mine] [--state] GET /api/docs/maintenance
10
+ *
11
+ * The door accepts an id or a slug, so there is no lookup first. Labels come back verbatim. Titles are shown to the
12
+ * person and never written to a log line.
13
+ */
14
+ import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
15
+ import { writeLine } from "./cli-io.js";
16
+ const TAG = "[docs refs cli]";
17
+ const DEADLINE_MS = 30_000;
18
+ const KIND = { doc: "page", ticket: "ticket", note: "meeting note" };
19
+ const list = (value) => (Array.isArray(value) ? value : []);
20
+ const str = (value) => (typeof value === "string" ? value : "");
21
+ export async function runDocsRefs(command, door) {
22
+ switch (command.action) {
23
+ case "templates": return templates(door);
24
+ case "create": return createFromTemplate(command, door);
25
+ case "refs": return refs(command, door);
26
+ case "review": return review(command, door);
27
+ case "maintenance": return maintenance(command, door);
28
+ default: return failAgentDoor(door, TAG, "invalid_body", "Try templates, create --template, refs, review or maintenance.");
29
+ }
30
+ }
31
+ async function templates(door) {
32
+ const answer = await askAgentDoor(door, { path: "/api/docs/templates", method: "GET", label: "docs templates", timeoutMs: DEADLINE_MS });
33
+ if (!answer.ok)
34
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
35
+ const body = answer.body;
36
+ const found = list(body["templates"]);
37
+ writeLine(door.io.stderr, `${TAG} templates ${JSON.stringify({ returned: found.length, templates_page: body["templates_page_id"] !== null })}`);
38
+ if (door.json)
39
+ return emitAgentDoor(door, { ok: true, ...body });
40
+ if (found.length === 0)
41
+ writeLine(door.io.stdout, str(body["hint"]) || "The templates page has no templates yet. Add a child page for each.");
42
+ for (const template of found)
43
+ writeLine(door.io.stdout, `${str(template["id"])} ${str(template["slug"]) || "-"} rev ${String(template["revision"])} ${str(template["title"])}`);
44
+ if (found.length > 0)
45
+ writeLine(door.io.stdout, `${found.length} template(s). Create one with: cockpit docs create --template <id|slug> --title "<title>".`);
46
+ return 0;
47
+ }
48
+ async function createFromTemplate(command, door) {
49
+ const answer = await askAgentDoor(door, {
50
+ path: "/api/docs/documents",
51
+ method: "POST",
52
+ label: "docs create from template",
53
+ timeoutMs: 60_000,
54
+ body: {
55
+ title: command.title,
56
+ template_id: command.docsRefs?.templateId,
57
+ ...(command.parentId ? { parent_id: command.parentId } : {}),
58
+ ...(command.visibility ? { visibility: command.visibility } : {}),
59
+ },
60
+ });
61
+ if (!answer.ok)
62
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
63
+ const body = answer.body;
64
+ const document = (body["document"] ?? {});
65
+ const recorded = body["provenance_recorded"] !== false;
66
+ writeLine(door.io.stderr, `${TAG} created ${JSON.stringify({ document_id: document["id"] ?? null, provenance_recorded: recorded })}`);
67
+ if (door.json)
68
+ return emitAgentDoor(door, { ok: true, document, template: body["template"] ?? null, provenance_recorded: recorded });
69
+ writeLine(door.io.stdout, `Created ${str(document["title"])} (${str(document["id"])}) from the template.`);
70
+ if (!recorded)
71
+ writeLine(door.io.stdout, "The page was created, but its template link could not be recorded (provenance_recorded: false).");
72
+ return 0;
73
+ }
74
+ async function refs(command, door) {
75
+ const answer = await askAgentDoor(door, { path: `/api/docs/documents/${encodeURIComponent(command.docRef ?? "")}/references`, method: "GET", label: "docs refs", timeoutMs: DEADLINE_MS });
76
+ if (!answer.ok)
77
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
78
+ const body = answer.body;
79
+ const backlinks = list(body["backlinks"]);
80
+ const outgoing = list(body["outgoing"]);
81
+ const coverage = (body["coverage"] ?? {});
82
+ writeLine(door.io.stderr, `${TAG} refs ${JSON.stringify({ document_id: body["document_id"] ?? null, backlinks: backlinks.length, outgoing: outgoing.length })}`);
83
+ if (door.json)
84
+ return emitAgentDoor(door, { ok: true, ...body });
85
+ writeLine(door.io.stdout, "Linked from:");
86
+ if (backlinks.length === 0)
87
+ writeLine(door.io.stdout, " nothing you can read links here");
88
+ for (const link of backlinks)
89
+ writeLine(door.io.stdout, ` ${str(link["id"])} ${str(link["title"])}`);
90
+ writeLine(door.io.stdout, "Links on this page:");
91
+ const readable = outgoing.filter((entry) => entry["state"] !== "unavailable");
92
+ if (readable.length === 0)
93
+ writeLine(door.io.stdout, " none");
94
+ for (const entry of readable)
95
+ writeLine(door.io.stdout, ` ${KIND[entry["kind"]] ?? str(entry["kind"])} ${str(entry["key"])} ${str(entry["title"])}`);
96
+ const gone = outgoing.filter((entry) => entry["state"] === "unavailable");
97
+ if (gone.length > 0) {
98
+ writeLine(door.io.stdout, "Not available to you:");
99
+ for (const entry of gone)
100
+ writeLine(door.io.stdout, ` ${KIND[entry["kind"]] ?? str(entry["kind"])} ${str(entry["key"])}`);
101
+ }
102
+ writeLine(door.io.stdout, `Coverage: linked from ${str((coverage["backlinks"] ?? {})["coverage"])}, links on this page ${str((coverage["outgoing"] ?? {})["coverage"])}.`);
103
+ return 0;
104
+ }
105
+ async function review(command, door) {
106
+ const given = command.docsRefs ?? {};
107
+ const write = given.owner !== undefined || given.everyDays !== undefined || given.reviewed === true;
108
+ const answer = await askAgentDoor(door, {
109
+ path: `/api/docs/documents/${encodeURIComponent(command.docRef ?? "")}/stewardship`,
110
+ method: write ? "PUT" : "GET",
111
+ label: write ? "docs review set" : "docs review",
112
+ timeoutMs: 60_000,
113
+ ...(write
114
+ ? { body: { ...(given.owner !== undefined ? { owner: given.owner } : {}), ...(given.everyDays !== undefined ? { review_every_days: given.everyDays } : {}), ...(given.reviewed ? { reviewed: true } : {}) } }
115
+ : {}),
116
+ });
117
+ if (!answer.ok)
118
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
119
+ const steward = (answer.body["stewardship"] ?? {});
120
+ writeLine(door.io.stderr, `${TAG} review ${JSON.stringify({ document_id: steward["document_id"] ?? null, write, state: steward["state"] ?? null })}`);
121
+ if (door.json)
122
+ return emitAgentDoor(door, { ok: true, stewardship: steward });
123
+ const owner = steward["owner"];
124
+ const reviewer = steward["last_reviewed_by"];
125
+ writeLine(door.io.stdout, `Owner: ${owner ? `${str(owner["name"])} <${str(owner["email"])}>` : "none"}`);
126
+ writeLine(door.io.stdout, `Review every: ${steward["review_every_days"] ? `${String(steward["review_every_days"])} days` : "no schedule"}`);
127
+ writeLine(door.io.stdout, `Last reviewed: ${steward["last_reviewed_at"] ? `${str(steward["last_reviewed_at"])} by ${reviewer ? str(reviewer["name"]) : "unknown"}` : "never"}`);
128
+ writeLine(door.io.stdout, `State: ${str(steward["state"])}${steward["review_due_at"] ? ` (due ${str(steward["review_due_at"])})` : ""}`);
129
+ return 0;
130
+ }
131
+ async function maintenance(command, door) {
132
+ const given = command.docsRefs ?? {};
133
+ const params = new URLSearchParams({ state: given.state ?? "all" });
134
+ if (given.mine)
135
+ params.set("owner", "me");
136
+ const answer = await askAgentDoor(door, { path: `/api/docs/maintenance?${params.toString()}`, method: "GET", label: "docs maintenance", timeoutMs: DEADLINE_MS });
137
+ if (!answer.ok)
138
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
139
+ const body = answer.body;
140
+ const items = list(body["items"]);
141
+ const coverage = (body["coverage"] ?? {});
142
+ writeLine(door.io.stderr, `${TAG} maintenance ${JSON.stringify({ returned: items.length, coverage: coverage["coverage"] ?? null })}`);
143
+ if (door.json)
144
+ return emitAgentDoor(door, { ok: true, ...body });
145
+ if (items.length === 0)
146
+ writeLine(door.io.stdout, "No page needs a person right now.");
147
+ for (const item of items) {
148
+ const owner = item["owner"];
149
+ writeLine(door.io.stdout, `${str(item["id"])} ${list(item["reasons"]).join("+") || str(item["state"])} ${owner ? str(owner["name"]) : "no owner"} ${str(item["title"])}`);
150
+ }
151
+ writeLine(door.io.stdout, `${items.length} page(s) shown from ${String(coverage["scanned"] ?? 0)} checked, coverage ${str(coverage["coverage"])}.`);
152
+ return 0;
153
+ }