@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.
- package/dist/adapters/git-state.js +1 -0
- package/dist/adapters/raw-evidence-budget-admission.js +63 -0
- package/dist/adapters/raw-evidence-collection-state.js +6 -50
- package/dist/adapters/raw-evidence-git-diff.js +1 -0
- package/dist/agent-rules.js +8 -0
- package/dist/autostart-contract.js +12 -0
- package/dist/autostart-darwin-plist.js +2 -2
- package/dist/autostart-darwin.js +1 -1
- package/dist/autostart-remind.js +245 -0
- package/dist/autostart-self-heal.js +17 -1
- package/dist/autostart-windows-scripts.js +7 -7
- package/dist/autostart.js +16 -15
- package/dist/commands/approvals-check-where.js +14 -0
- package/dist/commands/approvals-render.js +8 -1
- package/dist/commands/approvals.js +3 -2
- package/dist/commands/autostart-command.js +10 -0
- package/dist/commands/autostart-heal.js +36 -1
- package/dist/commands/brain.js +1 -1
- package/dist/commands/cal-agenda.js +44 -0
- package/dist/commands/cal-availability-share.js +97 -0
- package/dist/commands/cal-booking.js +147 -0
- package/dist/commands/cal-join.js +62 -0
- package/dist/commands/cal-links.js +68 -0
- package/dist/commands/cal-people.js +136 -0
- package/dist/commands/cal-remind.js +176 -0
- package/dist/commands/cal-reminders.js +33 -0
- package/dist/commands/cal-suggest.js +89 -0
- package/dist/commands/cal.js +41 -1
- package/dist/commands/cycle-args.js +127 -0
- package/dist/commands/cycles.js +117 -0
- package/dist/commands/docs-comments-args.js +85 -0
- package/dist/commands/docs-comments.js +128 -0
- package/dist/commands/docs-files-args.js +49 -0
- package/dist/commands/docs-files.js +179 -0
- package/dist/commands/docs-formatting.js +45 -0
- package/dist/commands/docs-inbox-args.js +93 -0
- package/dist/commands/docs-inbox.js +126 -0
- package/dist/commands/docs-notion.js +5 -0
- package/dist/commands/docs-read.js +2 -1
- package/dist/commands/docs-recovery.js +173 -0
- package/dist/commands/docs-refs-args.js +104 -0
- package/dist/commands/docs-refs.js +153 -0
- package/dist/commands/docs-search-args.js +79 -0
- package/dist/commands/docs-search.js +64 -0
- package/dist/commands/docs-sync-args.js +82 -0
- package/dist/commands/docs-sync.js +197 -0
- package/dist/commands/docs-tree.js +136 -0
- package/dist/commands/docs.js +85 -51
- package/dist/commands/doctor-update.js +4 -1
- package/dist/commands/inbox-args.js +81 -0
- package/dist/commands/inbox.js +88 -0
- package/dist/commands/issue-batch.js +61 -0
- package/dist/commands/issue-relations.js +151 -0
- package/dist/commands/issue-write.js +38 -5
- package/dist/commands/issue.js +32 -1
- package/dist/commands/local-args-collector-sources.js +2 -1
- package/dist/commands/local-args-tower-approvals.js +16 -4
- package/dist/commands/local-args-tower-cal-booking.js +81 -0
- package/dist/commands/local-args-tower-cal-links.js +42 -0
- package/dist/commands/local-args-tower-cal-remind.js +74 -0
- package/dist/commands/local-args-tower-cal-share.js +41 -0
- package/dist/commands/local-args-tower-cal.js +81 -8
- package/dist/commands/local-args-tower-docs-msg.js +98 -9
- package/dist/commands/local-args-tower-msg.js +222 -10
- package/dist/commands/local-args-tower-search.js +32 -5
- package/dist/commands/local-args-tower-work.js +110 -34
- package/dist/commands/local-auth.js +1 -1
- package/dist/commands/local-help-commands-tower-ops.js +6 -4
- package/dist/commands/local-help-commands-tower.js +47 -31
- package/dist/commands/local-help-commands.js +3 -1
- package/dist/commands/local-help-cycles.js +9 -0
- package/dist/commands/local-help-docs-files.js +10 -0
- package/dist/commands/local-help-docs-formatting.js +7 -0
- package/dist/commands/local-help-docs-inbox.js +10 -0
- package/dist/commands/local-help-docs-recovery.js +10 -0
- package/dist/commands/local-help-docs-refs.js +11 -0
- package/dist/commands/local-help-docs-sync.js +9 -0
- package/dist/commands/local-help-inbox.js +9 -0
- package/dist/commands/local-help-project.js +21 -0
- package/dist/commands/local-help-templates.js +9 -0
- package/dist/commands/local-help-views.js +10 -0
- package/dist/commands/local-help-work-notify.js +6 -0
- package/dist/commands/local-help.js +3 -3
- package/dist/commands/manager-hints.js +4 -2
- package/dist/commands/manager-limits.json +3 -1
- package/dist/commands/manager-read-reasons.js +1 -0
- package/dist/commands/manager-render-home.js +13 -5
- package/dist/commands/manager-render.js +3 -0
- package/dist/commands/manager.js +3 -1
- package/dist/commands/mcp-stdio-probe.js +1 -0
- package/dist/commands/msg-activity.js +134 -0
- package/dist/commands/msg-channel-args.js +92 -0
- package/dist/commands/msg-channel.js +125 -0
- package/dist/commands/msg-context-args.js +67 -0
- package/dist/commands/msg-context.js +65 -0
- package/dist/commands/msg-dm.js +28 -0
- package/dist/commands/msg-file.js +185 -0
- package/dist/commands/msg-mark-args.js +78 -0
- package/dist/commands/msg-mark.js +148 -0
- package/dist/commands/msg-notify-args.js +100 -0
- package/dist/commands/msg-notify.js +122 -0
- package/dist/commands/msg-schedule-args.js +114 -0
- package/dist/commands/msg-schedule.js +226 -0
- package/dist/commands/msg-search-args.js +71 -0
- package/dist/commands/msg-search.js +48 -0
- package/dist/commands/msg-write.js +15 -1
- package/dist/commands/msg.js +62 -39
- package/dist/commands/project-args.js +82 -0
- package/dist/commands/project.js +71 -7
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/search.js +23 -0
- package/dist/commands/session-sync-failures.js +17 -4
- package/dist/commands/session-sync-health.js +7 -2
- package/dist/commands/session-sync-hold.js +42 -1
- package/dist/commands/session-sync-outside-roots.js +72 -14
- package/dist/commands/session-sync-upload.js +2 -0
- package/dist/commands/session-sync.js +1 -0
- package/dist/commands/sources-check.js +63 -2
- package/dist/commands/sources-discovery.js +1 -1
- package/dist/commands/sources-reconciliation.js +87 -0
- package/dist/commands/sync-doctor.js +14 -1
- package/dist/commands/sync-report.js +1 -1
- package/dist/commands/template-args.js +122 -0
- package/dist/commands/templates.js +162 -0
- package/dist/commands/view-args.js +131 -0
- package/dist/commands/views.js +138 -0
- package/dist/commands/work-notify-args.js +67 -0
- package/dist/commands/work-notify.js +63 -0
- package/dist/delivery-hold-notice.js +34 -0
- package/dist/evidence-transcripts-client.js +1 -0
- package/dist/log-rotation.js +2 -2
- package/dist/notifier.js +83 -0
- package/dist/process-runner.js +4 -0
- package/dist/repo-identity-git.js +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
2
|
+
import { writeLine } from "./cli-io.js";
|
|
3
|
+
import { docsListPath, resolveDocId } from "./docs.js";
|
|
4
|
+
const TAG = "[docs cli]";
|
|
5
|
+
const READ_DEADLINE_MS = 30_000;
|
|
6
|
+
const WRITE_DEADLINE_MS = 60_000;
|
|
7
|
+
const WALK_MAX_PAGES = 20;
|
|
8
|
+
const PAGE_SIZE = 1000;
|
|
9
|
+
function renderTree(nodes, io, depth) {
|
|
10
|
+
for (const node of nodes) {
|
|
11
|
+
writeLine(io.stdout, `${" ".repeat(depth)}${node.title} (${node.slug ?? node.id})`);
|
|
12
|
+
if (node.children.length > 0)
|
|
13
|
+
renderTree(node.children, io, depth + 1);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function coverageSentence(coverage) {
|
|
17
|
+
const counts = `requested pages ${coverage.pages}, returned ${coverage.returned}, unique ${coverage.unique}`;
|
|
18
|
+
if (coverage.status === "exhausted")
|
|
19
|
+
return `Coverage: exhausted (${counts}).`;
|
|
20
|
+
if (coverage.status === "capped")
|
|
21
|
+
return `Coverage: capped at ${coverage.unique} documents; more exist (${counts}).`;
|
|
22
|
+
return `Coverage: partial, some pages could not load${coverage.reason ? ` (${coverage.reason})` : ""} (${counts}).`;
|
|
23
|
+
}
|
|
24
|
+
export async function treeDocs(door) {
|
|
25
|
+
const answer = await askAgentDoor(door, {
|
|
26
|
+
path: "/api/docs/tree",
|
|
27
|
+
method: "GET",
|
|
28
|
+
label: "docs tree",
|
|
29
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
30
|
+
});
|
|
31
|
+
if (!answer.ok)
|
|
32
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
33
|
+
const body = answer.body;
|
|
34
|
+
const tree = body.tree ?? [];
|
|
35
|
+
if (door.json)
|
|
36
|
+
return emitAgentDoor(door, { ok: true, tree, ...(body.coverage ? { coverage: body.coverage } : {}) });
|
|
37
|
+
if (tree.length === 0) {
|
|
38
|
+
writeLine(door.io.stdout, "No documents.");
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
renderTree(tree, door.io, 0);
|
|
42
|
+
}
|
|
43
|
+
if (body.coverage)
|
|
44
|
+
writeLine(door.io.stdout, coverageSentence(body.coverage));
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
/** `docs list --all`: walks every `?cursor=` page with the same filters, then prints the receipt. */
|
|
48
|
+
export async function listAllDocs(command, door) {
|
|
49
|
+
const documents = [];
|
|
50
|
+
const seen = new Set();
|
|
51
|
+
let returned = 0;
|
|
52
|
+
let pages = 0;
|
|
53
|
+
let cursor;
|
|
54
|
+
let status = "exhausted";
|
|
55
|
+
let reason;
|
|
56
|
+
for (;;) {
|
|
57
|
+
const path = docsListPath({ ...command, cursor, limit: PAGE_SIZE });
|
|
58
|
+
const answer = await askAgentDoor(door, { path, method: "GET", label: "docs list", timeoutMs: READ_DEADLINE_MS });
|
|
59
|
+
if (!answer.ok) {
|
|
60
|
+
if (pages === 0)
|
|
61
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
62
|
+
status = "failed_partial";
|
|
63
|
+
reason = answer.reason;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
const body = answer.body;
|
|
67
|
+
pages += 1;
|
|
68
|
+
for (const doc of body.documents ?? []) {
|
|
69
|
+
returned += 1;
|
|
70
|
+
if (seen.has(doc.id))
|
|
71
|
+
continue;
|
|
72
|
+
seen.add(doc.id);
|
|
73
|
+
documents.push(doc);
|
|
74
|
+
}
|
|
75
|
+
if (!body.next_cursor)
|
|
76
|
+
break;
|
|
77
|
+
if (pages >= WALK_MAX_PAGES) {
|
|
78
|
+
status = "capped";
|
|
79
|
+
reason = "page_ceiling";
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
cursor = body.next_cursor;
|
|
83
|
+
}
|
|
84
|
+
const coverage = { status, pages, returned, unique: seen.size, ...(reason ? { reason } : {}) };
|
|
85
|
+
writeLine(door.io.stderr, `${TAG} list-all ${JSON.stringify(coverage)}`);
|
|
86
|
+
if (door.json)
|
|
87
|
+
return emitAgentDoor(door, { ok: true, documents, coverage });
|
|
88
|
+
for (const doc of documents) {
|
|
89
|
+
const size = typeof doc.body_chars === "number" ? `${doc.body_chars}c`.padStart(8) : " ?";
|
|
90
|
+
writeLine(door.io.stdout, `${doc.id} ${doc.visibility.padEnd(7)} ${size} ${doc.slug ?? "(no slug)"} ${doc.title}`);
|
|
91
|
+
}
|
|
92
|
+
writeLine(door.io.stdout, "");
|
|
93
|
+
writeLine(door.io.stdout, `${documents.length} document(s). ${coverageSentence(coverage)}`);
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
export async function favoriteDocs(command, door) {
|
|
97
|
+
const on = command.action === "favorite";
|
|
98
|
+
const ref = command.docRef ?? "";
|
|
99
|
+
const resolved = await resolveDocId(door, ref);
|
|
100
|
+
if (resolved.status === "list_failed")
|
|
101
|
+
return failAgentDoor(door, TAG, resolved.reason, resolved.detail);
|
|
102
|
+
if (resolved.status === "not_found") {
|
|
103
|
+
return failAgentDoor(door, TAG, "document_not_found_or_unreadable", `That document does not exist, or you cannot read it: ${ref}`);
|
|
104
|
+
}
|
|
105
|
+
const answer = await askAgentDoor(door, {
|
|
106
|
+
path: `/api/docs/documents/${encodeURIComponent(resolved.id)}/favorite`,
|
|
107
|
+
method: on ? "PUT" : "DELETE",
|
|
108
|
+
label: on ? "docs favorite" : "docs unfavorite",
|
|
109
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
110
|
+
});
|
|
111
|
+
if (!answer.ok)
|
|
112
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
113
|
+
writeLine(door.io.stderr, `${TAG} favorite ${JSON.stringify({ document_id: resolved.id, favorite: on })}`);
|
|
114
|
+
if (door.json)
|
|
115
|
+
return emitAgentDoor(door, { ok: true, document_id: resolved.id, favorite: on });
|
|
116
|
+
writeLine(door.io.stdout, on ? `Favorited ${resolved.id}.` : `Removed ${resolved.id} from your favorites.`);
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
export async function marksDocs(door) {
|
|
120
|
+
const answer = await askAgentDoor(door, { path: "/api/docs/marks", method: "GET", label: "docs marks", timeoutMs: READ_DEADLINE_MS });
|
|
121
|
+
if (!answer.ok)
|
|
122
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
123
|
+
const body = answer.body;
|
|
124
|
+
const favorites = body.favorites ?? [];
|
|
125
|
+
const recents = body.recents ?? [];
|
|
126
|
+
if (door.json)
|
|
127
|
+
return emitAgentDoor(door, { ok: true, favorites, recents });
|
|
128
|
+
const print = (heading, rows) => {
|
|
129
|
+
writeLine(door.io.stdout, `${heading} (${rows.length})`);
|
|
130
|
+
for (const row of rows)
|
|
131
|
+
writeLine(door.io.stdout, ` ${row.id} ${row.marked_at} ${row.slug ?? "(no slug)"} ${row.title}`);
|
|
132
|
+
};
|
|
133
|
+
print("Favorites", favorites);
|
|
134
|
+
print("Recent", recents);
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
package/dist/commands/docs.js
CHANGED
|
@@ -31,8 +31,17 @@ import { randomUUID } from "node:crypto";
|
|
|
31
31
|
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
|
|
32
32
|
import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
|
|
33
33
|
import { readDoc } from "./docs-read.js";
|
|
34
|
+
import { searchDocs } from "./docs-search.js";
|
|
35
|
+
import { favoriteDocs, listAllDocs, marksDocs, treeDocs } from "./docs-tree.js";
|
|
34
36
|
import { exportDoc } from "./docs-export.js";
|
|
35
37
|
import { linkDoc, syncStatusDocs } from "./docs-notion.js";
|
|
38
|
+
import { restoreDocs, trashDocs, versionsDocs } from "./docs-recovery.js";
|
|
39
|
+
import { commentDocs, readDocComments } from "./docs-comments.js";
|
|
40
|
+
import { runDocsFiles } from "./docs-files.js";
|
|
41
|
+
import { runDocsRefs } from "./docs-refs.js";
|
|
42
|
+
import { runDocsSync } from "./docs-sync.js";
|
|
43
|
+
import { runDocsFormatting } from "./docs-formatting.js";
|
|
44
|
+
import { runDocsInbox } from "./docs-inbox.js";
|
|
36
45
|
import { accessDocs, shareDocs } from "./docs-share.js";
|
|
37
46
|
import { readNoteFile } from "./notes-file.js";
|
|
38
47
|
import { cacheAgeLabel, cacheCoverage, readThroughCache } from "./read-cache.js";
|
|
@@ -45,12 +54,19 @@ export async function runDocs(command, io) {
|
|
|
45
54
|
switch (command.action) {
|
|
46
55
|
case "list":
|
|
47
56
|
return listDocs(command, door);
|
|
57
|
+
case "search":
|
|
58
|
+
return searchDocs(command, door);
|
|
48
59
|
case "tree":
|
|
49
60
|
return treeDocs(door);
|
|
61
|
+
case "favorite":
|
|
62
|
+
case "unfavorite":
|
|
63
|
+
return favoriteDocs(command, door);
|
|
64
|
+
case "marks":
|
|
65
|
+
return marksDocs(door);
|
|
50
66
|
case "read":
|
|
51
67
|
return readDoc(command, door);
|
|
52
68
|
case "create":
|
|
53
|
-
return createDoc(command, door);
|
|
69
|
+
return command.docsRefs?.templateId ? runDocsRefs(command, door) : createDoc(command, door);
|
|
54
70
|
case "update":
|
|
55
71
|
return updateDoc(command, door);
|
|
56
72
|
case "delete":
|
|
@@ -67,6 +83,35 @@ export async function runDocs(command, io) {
|
|
|
67
83
|
return shareDocs(command, door);
|
|
68
84
|
case "access":
|
|
69
85
|
return accessDocs(command, door);
|
|
86
|
+
case "comments":
|
|
87
|
+
return readDocComments(command, door);
|
|
88
|
+
case "comment":
|
|
89
|
+
return commentDocs(command, door);
|
|
90
|
+
case "inbox":
|
|
91
|
+
case "inbox-read":
|
|
92
|
+
case "subscribe":
|
|
93
|
+
case "unsubscribe":
|
|
94
|
+
case "notifications":
|
|
95
|
+
return runDocsInbox(command, door);
|
|
96
|
+
case "files":
|
|
97
|
+
case "file":
|
|
98
|
+
return runDocsFiles(command, door);
|
|
99
|
+
case "formatting":
|
|
100
|
+
return runDocsFormatting(command, door);
|
|
101
|
+
case "templates":
|
|
102
|
+
case "refs":
|
|
103
|
+
case "review":
|
|
104
|
+
case "maintenance":
|
|
105
|
+
return runDocsRefs(command, door);
|
|
106
|
+
case "sync-resolve":
|
|
107
|
+
case "manifest":
|
|
108
|
+
return runDocsSync(command, door);
|
|
109
|
+
case "trash":
|
|
110
|
+
return trashDocs(command, door);
|
|
111
|
+
case "restore":
|
|
112
|
+
return restoreDocs(command, door);
|
|
113
|
+
case "versions":
|
|
114
|
+
return versionsDocs(command, door);
|
|
70
115
|
}
|
|
71
116
|
}
|
|
72
117
|
/** `--parent`, `--query`, `--word`, `--limit`, `--bodies` (BLI-10450) as the door's own query parameters (BLI-3737). */
|
|
@@ -82,10 +127,14 @@ export function docsListPath(command) {
|
|
|
82
127
|
params.set("limit", String(command.limit));
|
|
83
128
|
if (command.bodies)
|
|
84
129
|
params.set("bodies", "true");
|
|
130
|
+
if (command.cursor)
|
|
131
|
+
params.set("cursor", command.cursor);
|
|
85
132
|
const search = params.toString();
|
|
86
133
|
return search.length > 0 ? `/api/docs/documents?${search}` : "/api/docs/documents";
|
|
87
134
|
}
|
|
88
135
|
async function listDocs(command, door) {
|
|
136
|
+
if (command.all)
|
|
137
|
+
return listAllDocs(command, door);
|
|
89
138
|
const listPath = docsListPath(command);
|
|
90
139
|
const answer = await readThroughCache({
|
|
91
140
|
io: door.io,
|
|
@@ -125,32 +174,9 @@ async function listDocs(command, door) {
|
|
|
125
174
|
}
|
|
126
175
|
writeLine(door.io.stdout, "");
|
|
127
176
|
writeLine(door.io.stdout, `${documents.length} document(s). Bodies come from \`cockpit docs read <id|slug>\`.`);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
for (const node of nodes) {
|
|
132
|
-
writeLine(io.stdout, `${" ".repeat(depth)}${node.title} (${node.slug ?? node.id})`);
|
|
133
|
-
if (node.children.length > 0)
|
|
134
|
-
renderTree(node.children, io, depth + 1);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
async function treeDocs(door) {
|
|
138
|
-
const answer = await askAgentDoor(door, {
|
|
139
|
-
path: "/api/docs/tree",
|
|
140
|
-
method: "GET",
|
|
141
|
-
label: "docs tree",
|
|
142
|
-
timeoutMs: READ_DEADLINE_MS,
|
|
143
|
-
});
|
|
144
|
-
if (!answer.ok)
|
|
145
|
-
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
146
|
-
const tree = (answer.body.tree ?? []);
|
|
147
|
-
if (door.json)
|
|
148
|
-
return emitAgentDoor(door, { ok: true, tree });
|
|
149
|
-
if (tree.length === 0) {
|
|
150
|
-
writeLine(door.io.stdout, "No documents.");
|
|
151
|
-
return 0;
|
|
152
|
-
}
|
|
153
|
-
renderTree(tree, door.io, 0);
|
|
177
|
+
const nextCursor = answer.body.next_cursor;
|
|
178
|
+
if (nextCursor)
|
|
179
|
+
writeLine(door.io.stdout, `More: cockpit docs list --cursor ${nextCursor} (or --all to walk every page)`);
|
|
154
180
|
return 0;
|
|
155
181
|
}
|
|
156
182
|
/** Finds a document id by exact id or exact slug against the flat list. Never fuzzy. */
|
|
@@ -161,30 +187,39 @@ export async function resolveDocId(door, ref) {
|
|
|
161
187
|
const id = resolved.ids.get(ref);
|
|
162
188
|
return id ? { status: "ok", id } : { status: "not_found" };
|
|
163
189
|
}
|
|
190
|
+
const RESOLVE_CONCURRENCY = 8;
|
|
164
191
|
/**
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
192
|
+
* Resolves every ref with ONE exact lookup each (BLI-21657): `GET /api/docs/documents/<ref>?fields=revision`
|
|
193
|
+
* answers the real id for an id or a slug (slug match is exact, case-insensitive, never fuzzy). Never a scan
|
|
194
|
+
* of the capped list, so the oldest page resolves like the newest. At most 8 lookups in flight. A ref the
|
|
195
|
+
* door answers 404 for is simply absent from `ids`; any other failure is `list_failed` with the door's reason.
|
|
168
196
|
*/
|
|
169
197
|
export async function resolveDocIds(door, refs) {
|
|
170
|
-
const
|
|
171
|
-
path: "/api/docs/documents",
|
|
172
|
-
method: "GET",
|
|
173
|
-
label: "docs resolve",
|
|
174
|
-
timeoutMs: READ_DEADLINE_MS,
|
|
175
|
-
});
|
|
176
|
-
if (!answer.ok)
|
|
177
|
-
return { status: "list_failed", reason: answer.reason, detail: answer.detail };
|
|
178
|
-
const documents = (answer.body.documents ?? []);
|
|
179
|
-
const byId = new Map(documents.map((doc) => [doc.id, doc.id]));
|
|
180
|
-
const bySlug = new Map(documents.filter((doc) => doc.slug).map((doc) => [doc.slug, doc.id]));
|
|
198
|
+
const unique = [...new Set(refs)];
|
|
181
199
|
const ids = new Map();
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
200
|
+
let failure = null;
|
|
201
|
+
let next = 0;
|
|
202
|
+
const worker = async () => {
|
|
203
|
+
while (failure === null && next < unique.length) {
|
|
204
|
+
const ref = unique[next++];
|
|
205
|
+
const answer = await askAgentDoor(door, {
|
|
206
|
+
path: `/api/docs/documents/${encodeURIComponent(ref)}?fields=revision`,
|
|
207
|
+
method: "GET",
|
|
208
|
+
label: "docs resolve",
|
|
209
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
210
|
+
});
|
|
211
|
+
if (answer.ok) {
|
|
212
|
+
const id = answer.body.document?.id;
|
|
213
|
+
if (id)
|
|
214
|
+
ids.set(ref, id);
|
|
215
|
+
}
|
|
216
|
+
else if (answer.reason !== "document_not_found_or_unreadable") {
|
|
217
|
+
failure = { reason: answer.reason, detail: answer.detail };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
await Promise.all(Array.from({ length: Math.min(RESOLVE_CONCURRENCY, unique.length) }, worker));
|
|
222
|
+
return failure ? { status: "list_failed", ...failure } : { status: "ok", ids };
|
|
188
223
|
}
|
|
189
224
|
/** Body on stdin, never argv (BLI-3706) — `--file` is the safest way on Windows, same as `cockpit notes paste`. */
|
|
190
225
|
async function readBody(command, io) {
|
|
@@ -296,8 +331,7 @@ async function mutationStatusDocs(command, door) {
|
|
|
296
331
|
/**
|
|
297
332
|
* BLI‑10555: soft-deletes one or more documents — `DELETE
|
|
298
333
|
* /api/docs/documents/[id]`, the same door the browser's own delete calls.
|
|
299
|
-
* Recoverable (
|
|
300
|
-
* self-service undo) and never cascades: a deleted document's children keep
|
|
334
|
+
* Recoverable (`cockpit docs trash` lists it, `docs restore <id>` brings it back) and never cascades: a deleted document's children keep
|
|
301
335
|
* their own `parent_id` and stay exactly where they were.
|
|
302
336
|
*
|
|
303
337
|
* Mirrors `readDoc`'s multi-ref shape (BLI-10450): one shared `docs list`
|
|
@@ -319,7 +353,7 @@ async function deleteDocs(command, door) {
|
|
|
319
353
|
}
|
|
320
354
|
if (door.json)
|
|
321
355
|
return emitAgentDoor(door, { ok: true, document_id: result.id });
|
|
322
|
-
writeLine(door.io.stdout, `Deleted ${result.id}.
|
|
356
|
+
writeLine(door.io.stdout, `Deleted ${result.id}. Restore it with: cockpit docs restore ${result.id} (its children stay in place).`);
|
|
323
357
|
return 0;
|
|
324
358
|
}
|
|
325
359
|
if (door.json) {
|
|
@@ -340,7 +374,7 @@ async function deleteDocs(command, door) {
|
|
|
340
374
|
}
|
|
341
375
|
});
|
|
342
376
|
writeLine(door.io.stdout, "");
|
|
343
|
-
writeLine(door.io.stdout, `${results.length - failed.length}/${results.length} document(s) deleted.
|
|
377
|
+
writeLine(door.io.stdout, `${results.length - failed.length}/${results.length} document(s) deleted. Restore with: cockpit docs restore <id> (children stay in place).`);
|
|
344
378
|
return failed.length === 0 ? 0 : 1;
|
|
345
379
|
}
|
|
346
380
|
async function deleteOneDocument(door, ref, id) {
|
|
@@ -51,8 +51,11 @@ export async function latestCliVersionFromNpm(context) {
|
|
|
51
51
|
? `@bli-cockpit/cli@${context.command.updateTag}`
|
|
52
52
|
: "@bli-cockpit/cli";
|
|
53
53
|
const result = await exec("npm", ["view", packageSpec, "version", "--json"]);
|
|
54
|
-
if (result.code !== 0)
|
|
54
|
+
if (result.code !== 0) {
|
|
55
|
+
// Ticket 21952: this branch was silent, so "npm could not even start" read the same as "npm is unreachable".
|
|
56
|
+
console.error("[doctor] npm latest unknown", JSON.stringify({ reason: "npm_view_failed", exit_code: result.code, stderr_bytes: result.stderr.length }));
|
|
55
57
|
return null;
|
|
58
|
+
}
|
|
56
59
|
return parseNpmVersion(result.stdout);
|
|
57
60
|
}
|
|
58
61
|
function parseNpmVersion(stdout) {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit ticket inbox|read|unread|subscribe|unsubscribe` argument parsing (BLI-21703, P9). These are
|
|
3
|
+
* `issue` actions; their flags are read here so `local-args-tower-work.ts` only lists the names.
|
|
4
|
+
*/
|
|
5
|
+
import { optionalNonEmpty, optionalPositiveInteger, optionalUrl } from "./local-arg-values.js";
|
|
6
|
+
export const INBOX_ACTIONS = ["inbox", "read", "unread", "subscribe", "unsubscribe"];
|
|
7
|
+
export const INBOX_VALUE_FLAGS = ["--kind", "--before"];
|
|
8
|
+
export const INBOX_BOOLEAN_FLAGS = ["--unread"];
|
|
9
|
+
export const INBOX_KINDS = ["assigned", "mentioned", "commented"];
|
|
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
|
+
export function isInboxAction(action) {
|
|
12
|
+
return INBOX_ACTIONS.includes(action);
|
|
13
|
+
}
|
|
14
|
+
export function parseInboxArgs(values, action, rest) {
|
|
15
|
+
const given = (flag) => values.flags.has(flag) || values.booleans.has(flag);
|
|
16
|
+
if (action !== "inbox") {
|
|
17
|
+
for (const flag of [...INBOX_VALUE_FLAGS, ...INBOX_BOOLEAN_FLAGS, "--limit"]) {
|
|
18
|
+
if (given(flag))
|
|
19
|
+
throw new Error(`${flag} belongs to \`cockpit ticket inbox\`.`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (action !== "read" && action !== "inbox" && given("--all"))
|
|
23
|
+
throw new Error("--all belongs to `cockpit ticket read`.");
|
|
24
|
+
if (given("--request-id") || given("--expected-revision"))
|
|
25
|
+
throw new Error(`ticket ${action} takes no --request-id or --expected-revision.`);
|
|
26
|
+
const inbox = {};
|
|
27
|
+
if (action === "inbox") {
|
|
28
|
+
if (rest.length > 0)
|
|
29
|
+
throw new Error(`ticket inbox does not take "${rest[0]}".`);
|
|
30
|
+
if (given("--all"))
|
|
31
|
+
throw new Error("--all belongs to `cockpit ticket read`. Page the inbox with --before and --limit.");
|
|
32
|
+
if (values.booleans.has("--unread"))
|
|
33
|
+
inbox.unreadOnly = true;
|
|
34
|
+
const kind = optionalNonEmpty(values.flags.get("--kind"));
|
|
35
|
+
if (kind) {
|
|
36
|
+
if (!INBOX_KINDS.includes(kind))
|
|
37
|
+
throw new Error(`--kind must be ${INBOX_KINDS.join(", ")}.`);
|
|
38
|
+
inbox.kind = kind;
|
|
39
|
+
}
|
|
40
|
+
const before = optionalNonEmpty(values.flags.get("--before"));
|
|
41
|
+
if (before)
|
|
42
|
+
inbox.before = before;
|
|
43
|
+
const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
|
|
44
|
+
if (limit !== undefined) {
|
|
45
|
+
if (limit > 100)
|
|
46
|
+
throw new Error("--limit is at most 100.");
|
|
47
|
+
inbox.limit = limit;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else if (action === "read" || action === "unread") {
|
|
51
|
+
if (action === "read" && given("--all")) {
|
|
52
|
+
if (rest.length > 0)
|
|
53
|
+
throw new Error("ticket read takes notification ids or --all, not both.");
|
|
54
|
+
inbox.all = true;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
if (rest.length === 0)
|
|
58
|
+
throw new Error(`ticket ${action} needs one or more notification ids${action === "read" ? " or --all" : ""}.`);
|
|
59
|
+
if (rest.length > 200)
|
|
60
|
+
throw new Error("Mark at most 200 items at a time.");
|
|
61
|
+
for (const id of rest) {
|
|
62
|
+
if (!UUID.test(id))
|
|
63
|
+
throw new Error(`"${id.slice(0, 40)}" is not a notification id. Copy the id from \`cockpit ticket inbox --json\`.`);
|
|
64
|
+
}
|
|
65
|
+
inbox.ids = rest;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
if (rest.length !== 1)
|
|
70
|
+
throw new Error(`ticket ${action} takes exactly one ticket reference.`);
|
|
71
|
+
inbox.ref = rest[0];
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
kind: "issue",
|
|
75
|
+
action,
|
|
76
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
77
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
78
|
+
json: values.booleans.has("--json"),
|
|
79
|
+
inbox,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
2
|
+
import { writeLine } from "./cli-io.js";
|
|
3
|
+
import { READ_DEADLINE_MS, WRITE_DEADLINE_MS } from "./issue-contracts.js";
|
|
4
|
+
const TAG = "[ticket inbox cli]";
|
|
5
|
+
const unreadText = (counts) => counts ? `${counts.unread}${counts.capped ? "+" : ""} unread` : "unread count unavailable";
|
|
6
|
+
export async function runInbox(command, io, door) {
|
|
7
|
+
void io;
|
|
8
|
+
const inbox = command.inbox;
|
|
9
|
+
switch (command.action) {
|
|
10
|
+
case "inbox":
|
|
11
|
+
return readInbox(command, door);
|
|
12
|
+
case "read":
|
|
13
|
+
case "unread": {
|
|
14
|
+
const read = command.action === "read";
|
|
15
|
+
const answer = await askAgentDoor(door, {
|
|
16
|
+
path: "/api/work/inbox/read",
|
|
17
|
+
method: "POST",
|
|
18
|
+
label: `ticket ${command.action}`,
|
|
19
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
20
|
+
body: inbox.all ? { all: true, read: true } : { ids: inbox.ids, read },
|
|
21
|
+
});
|
|
22
|
+
if (!answer.ok)
|
|
23
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
24
|
+
const body = answer.body;
|
|
25
|
+
console.error(`${TAG} ${command.action} done`, JSON.stringify({ requested: body.requested, updated: body.updated }));
|
|
26
|
+
if (door.json)
|
|
27
|
+
return emitAgentDoor(door, { ok: true, ...body });
|
|
28
|
+
writeLine(door.io.stdout, `Marked ${body.updated ?? 0} item(s) ${read ? "read" : "unread"}. ${unreadText(body.counts)}.`);
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
default: {
|
|
32
|
+
const follow = command.action === "subscribe";
|
|
33
|
+
const answer = await askAgentDoor(door, {
|
|
34
|
+
path: `/api/work/issues/${encodeURIComponent(inbox.ref ?? "")}/follow`,
|
|
35
|
+
method: follow ? "PUT" : "DELETE",
|
|
36
|
+
label: `ticket ${command.action}`,
|
|
37
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
38
|
+
});
|
|
39
|
+
if (!answer.ok)
|
|
40
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
41
|
+
const body = answer.body;
|
|
42
|
+
console.error(`${TAG} ${command.action} done`, JSON.stringify({ following: body.follow?.following }));
|
|
43
|
+
if (door.json)
|
|
44
|
+
return emitAgentDoor(door, { ok: true, ...body });
|
|
45
|
+
writeLine(door.io.stdout, follow ? `Following ${inbox.ref}.` : `Unfollowed ${inbox.ref}. Direct assignments and @mentions still arrive.`);
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function readInbox(command, door) {
|
|
51
|
+
const inbox = command.inbox;
|
|
52
|
+
const query = new URLSearchParams();
|
|
53
|
+
if (inbox.unreadOnly)
|
|
54
|
+
query.set("unread", "true");
|
|
55
|
+
if (inbox.kind)
|
|
56
|
+
query.set("kind", inbox.kind);
|
|
57
|
+
if (inbox.before)
|
|
58
|
+
query.set("before", inbox.before);
|
|
59
|
+
if (inbox.limit !== undefined)
|
|
60
|
+
query.set("limit", String(inbox.limit));
|
|
61
|
+
const suffix = query.toString();
|
|
62
|
+
const answer = await askAgentDoor(door, {
|
|
63
|
+
path: `/api/work/inbox${suffix ? `?${suffix}` : ""}`,
|
|
64
|
+
method: "GET",
|
|
65
|
+
label: "ticket inbox",
|
|
66
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
67
|
+
});
|
|
68
|
+
if (!answer.ok)
|
|
69
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
70
|
+
const body = answer.body;
|
|
71
|
+
if (door.json)
|
|
72
|
+
return emitAgentDoor(door, { ok: true, ...body });
|
|
73
|
+
const out = door.io.stdout;
|
|
74
|
+
const items = body.items ?? [];
|
|
75
|
+
if (items.length === 0) {
|
|
76
|
+
writeLine(out, `${inbox.unreadOnly ? "No unread items" : "Your inbox is empty"}. ${unreadText(body.counts)}.`);
|
|
77
|
+
}
|
|
78
|
+
for (const item of items) {
|
|
79
|
+
writeLine(out, `${item.read_at ? " " : "*"} ${item.id} ${item.kind.padEnd(9)} ${item.identifier.padEnd(9)} ${item.title} (${item.created_at})`);
|
|
80
|
+
}
|
|
81
|
+
if (items.length > 0)
|
|
82
|
+
writeLine(out, `${unreadText(body.counts)}.`);
|
|
83
|
+
if (body.coverage?.dropped)
|
|
84
|
+
writeLine(out, `${body.coverage.dropped} item(s) are not shown: you cannot read their ticket.`);
|
|
85
|
+
if (body.has_more && body.next_cursor)
|
|
86
|
+
writeLine(out, `More items available. Continue with --before ${body.next_cursor}`);
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* `cockpit ticket batch` (P5, BLI-21551): change up to 50 tickets in one call.
|
|
4
|
+
*
|
|
5
|
+
* Items come from stdin or `--file` (`{ "items": [...] }` or a bare array),
|
|
6
|
+
* never argv. The door applies each item as its own mutation and answers per
|
|
7
|
+
* item; this verb prints one stderr line per item and exits 0 only when every
|
|
8
|
+
* item committed. The batch request id is printed so a retry of the same body
|
|
9
|
+
* returns the first receipts instead of writing again.
|
|
10
|
+
*/
|
|
11
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
12
|
+
import { writeLine } from "./cli-io.js";
|
|
13
|
+
import { TAG, WRITE_DEADLINE_MS, readBody } from "./issue-contracts.js";
|
|
14
|
+
export async function batchIssues(command, door) {
|
|
15
|
+
const body = await readBody(command, door.io);
|
|
16
|
+
if (!body.ok)
|
|
17
|
+
return failAgentDoor(door, TAG, body.reason, body.detail);
|
|
18
|
+
if (body.text.trim() === "") {
|
|
19
|
+
return failAgentDoor(door, TAG, "invalid_body", "issue batch reads { \"items\": [...] } from stdin or --file; nothing was given.");
|
|
20
|
+
}
|
|
21
|
+
let parsed;
|
|
22
|
+
try {
|
|
23
|
+
parsed = JSON.parse(body.text);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return failAgentDoor(door, TAG, "invalid_json_body", "issue batch needs JSON: { \"items\": [{ \"issue\": \"BLI-1\", \"priority\": 2 }] } or a bare array.");
|
|
27
|
+
}
|
|
28
|
+
const items = Array.isArray(parsed) ? parsed : parsed?.items;
|
|
29
|
+
if (!Array.isArray(items))
|
|
30
|
+
return failAgentDoor(door, TAG, "invalid_body", "issue batch needs a list of items.");
|
|
31
|
+
const requestId = command.requestId ?? randomUUID();
|
|
32
|
+
writeLine(door.io.stderr, `${TAG} batch request ${JSON.stringify({ request_id: requestId, items: items.length })}`);
|
|
33
|
+
let answer;
|
|
34
|
+
try {
|
|
35
|
+
answer = await askAgentDoor(door, {
|
|
36
|
+
path: "/api/work/issues/batch", method: "POST", label: "issue batch", timeoutMs: WRITE_DEADLINE_MS,
|
|
37
|
+
body: { request_id: requestId, items },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
console.error(`${TAG} batch response lost`, JSON.stringify({ reason: "mutation_unknown", request_id: requestId }));
|
|
42
|
+
answer = { ok: false, reason: "mutation_unknown", detail: "Result unknown. Retry the same input with the same --request-id." };
|
|
43
|
+
}
|
|
44
|
+
if (!answer.ok) {
|
|
45
|
+
writeLine(door.io.stderr, `${TAG} retry unchanged input with --request-id ${requestId}; nothing is written twice.`);
|
|
46
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
47
|
+
}
|
|
48
|
+
const result = answer.body;
|
|
49
|
+
for (const item of result.items ?? []) {
|
|
50
|
+
const why = item.reason ? ` ${item.reason}${item.message ? ` - ${item.message}` : ""}` : "";
|
|
51
|
+
writeLine(door.io.stderr, `${TAG} ${item.issue} ${item.outcome}${why}`);
|
|
52
|
+
}
|
|
53
|
+
writeLine(door.io.stderr, `${TAG} ${result.committed} committed, ${result.refused} refused, ${result.unknown} unknown of ${result.requested}. batch request id ${result.request_id}`);
|
|
54
|
+
const allCommitted = result.committed === result.requested;
|
|
55
|
+
if (!allCommitted)
|
|
56
|
+
writeLine(door.io.stderr, `${TAG} to retry the same body use --request-id ${result.request_id}.`);
|
|
57
|
+
if (door.json)
|
|
58
|
+
return emitAgentDoor(door, { ok: allCommitted, ...result }, allCommitted ? 0 : 1);
|
|
59
|
+
writeLine(door.io.stdout, `${result.committed} of ${result.requested} tickets changed.`);
|
|
60
|
+
return allCommitted ? 0 : 1;
|
|
61
|
+
}
|