@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,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
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The docs-search flags shared by `cockpit docs search` and `cockpit search --kind doc` (BLI-21697, docs parity
|
|
3
|
+
* step 6): `--title-only`, `--word`, `--by <me|email|id>`, `--under <id|slug>`, `--since` / `--until`
|
|
4
|
+
* (`YYYY-MM-DD`, inclusive local days on `updated_at`), `--timezone <IANA>` and `--sort relevance|updated`. One
|
|
5
|
+
* reader and one query-string writer, so the two commands cannot mean different things. The door checks the
|
|
6
|
+
* values; this file only checks shape.
|
|
7
|
+
*/
|
|
8
|
+
import { optionalNonEmpty } from "./local-arg-values.js";
|
|
9
|
+
export const DOC_SEARCH_VALUE_FLAGS = ["--by", "--under", "--sort"];
|
|
10
|
+
export const DOC_SEARCH_BOOLEAN_FLAGS = ["--title-only", "--word"];
|
|
11
|
+
/** Flags only a docs search reads (`--since`, `--until` and `--timezone` are shared with the message search). */
|
|
12
|
+
export const DOC_SEARCH_ONLY_FLAGS = [...DOC_SEARCH_VALUE_FLAGS, ...DOC_SEARCH_BOOLEAN_FLAGS];
|
|
13
|
+
const DAY = /^\d{4}-\d{2}-\d{2}$/;
|
|
14
|
+
function machineZone() {
|
|
15
|
+
try {
|
|
16
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return "UTC";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function day(flag, value) {
|
|
23
|
+
const given = optionalNonEmpty(value);
|
|
24
|
+
if (given === undefined)
|
|
25
|
+
return undefined;
|
|
26
|
+
if (!DAY.test(given))
|
|
27
|
+
throw new Error(`${flag} takes a day written YYYY-MM-DD, not "${given}".`);
|
|
28
|
+
return given;
|
|
29
|
+
}
|
|
30
|
+
/** Reads the flags. The machine's IANA zone is sent unless `--timezone` is given. */
|
|
31
|
+
export function readDocSearchFilters(values) {
|
|
32
|
+
const since = day("--since", values.flags.get("--since"));
|
|
33
|
+
const until = day("--until", values.flags.get("--until"));
|
|
34
|
+
if (since && until && since > until)
|
|
35
|
+
throw new Error(`--since ${since} is after --until ${until}.`);
|
|
36
|
+
const sort = optionalNonEmpty(values.flags.get("--sort"));
|
|
37
|
+
if (sort !== undefined && sort !== "relevance" && sort !== "updated") {
|
|
38
|
+
throw new Error(`--sort takes relevance or updated, not "${sort}".`);
|
|
39
|
+
}
|
|
40
|
+
const by = optionalNonEmpty(values.flags.get("--by"));
|
|
41
|
+
const under = optionalNonEmpty(values.flags.get("--under"));
|
|
42
|
+
return {
|
|
43
|
+
...(values.booleans.has("--title-only") ? { titleOnly: true } : {}),
|
|
44
|
+
...(values.booleans.has("--word") ? { wholeWord: true } : {}),
|
|
45
|
+
...(by ? { by } : {}),
|
|
46
|
+
...(under ? { under } : {}),
|
|
47
|
+
...(since ? { since } : {}),
|
|
48
|
+
...(until ? { until } : {}),
|
|
49
|
+
timezone: optionalNonEmpty(values.flags.get("--timezone")) ?? machineZone(),
|
|
50
|
+
...(sort ? { sort } : {}),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** True when a flag that narrows by person, subtree or date was given (an empty query is then allowed). */
|
|
54
|
+
export function hasDocScopeFlags(filters) {
|
|
55
|
+
return Boolean(filters.by || filters.under || filters.since || filters.until);
|
|
56
|
+
}
|
|
57
|
+
/** True when any docs-only flag was given (so `cockpit search` can refuse them without `--kind doc`). */
|
|
58
|
+
export function hasDocSearchFlags(filters) {
|
|
59
|
+
return Boolean(filters.titleOnly || filters.wholeWord || filters.sort || hasDocScopeFlags(filters));
|
|
60
|
+
}
|
|
61
|
+
/** The query parameters `GET /api/docs/search` and `GET /api/search` read. */
|
|
62
|
+
export function appendDocSearchParams(params, filters) {
|
|
63
|
+
if (filters.titleOnly)
|
|
64
|
+
params.set("title_only", "1");
|
|
65
|
+
if (filters.wholeWord)
|
|
66
|
+
params.set("word", "1");
|
|
67
|
+
if (filters.by)
|
|
68
|
+
params.set("by", filters.by);
|
|
69
|
+
if (filters.under)
|
|
70
|
+
params.set("under", filters.under);
|
|
71
|
+
if (filters.since)
|
|
72
|
+
params.set("since", filters.since);
|
|
73
|
+
if (filters.until)
|
|
74
|
+
params.set("until", filters.until);
|
|
75
|
+
if (filters.timezone)
|
|
76
|
+
params.set("tz", filters.timezone);
|
|
77
|
+
if (filters.sort)
|
|
78
|
+
params.set("sort", filters.sort);
|
|
79
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit docs search` (BLI-21697, docs parity step 6): the library searched by title or text, narrowed by
|
|
3
|
+
* person, subtree and day, over `GET /api/docs/search` — the SAME door the web box calls, so the ids match the
|
|
4
|
+
* browser, MCP `docs_search` and JARVIS `searchDocs`. Nothing is matched on this side.
|
|
5
|
+
*
|
|
6
|
+
* It prints the hits, `matched/returned` and the coverage line. A walk that stopped early is said out loud
|
|
7
|
+
* (`capped`, `failed_partial`), never folded into "nothing matched". A door refusal keeps its own `reason` and
|
|
8
|
+
* sentence (`invalid_filter` names the bad field).
|
|
9
|
+
*/
|
|
10
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
11
|
+
import { writeLine } from "./cli-io.js";
|
|
12
|
+
import { appendDocSearchParams } from "./docs-search-args.js";
|
|
13
|
+
const TAG = "[docs search cli]";
|
|
14
|
+
const READ_DEADLINE_MS = 30_000;
|
|
15
|
+
export function docsSearchPath(command) {
|
|
16
|
+
const params = new URLSearchParams();
|
|
17
|
+
if (command.query)
|
|
18
|
+
params.set("q", command.query);
|
|
19
|
+
appendDocSearchParams(params, command.searchFilters ?? {});
|
|
20
|
+
if (command.limit !== undefined)
|
|
21
|
+
params.set("limit", String(command.limit));
|
|
22
|
+
return `/api/docs/search?${params.toString()}`;
|
|
23
|
+
}
|
|
24
|
+
/** The sentence under the hits: the walk's honesty, in words. */
|
|
25
|
+
export function docsSearchCoverageLine(body) {
|
|
26
|
+
const counts = `${body.returned ?? 0} of ${body.matched ?? 0} shown`;
|
|
27
|
+
if (body.coverage === "capped")
|
|
28
|
+
return `Coverage: capped after ${body.searched ?? 0} pages searched; more exist (${counts}).`;
|
|
29
|
+
if (body.coverage === "failed_partial") {
|
|
30
|
+
return `Coverage: partial, some pages could not be searched${body.coverage_reason ? ` (${body.coverage_reason})` : ""} (${counts}).`;
|
|
31
|
+
}
|
|
32
|
+
return `Coverage: exhausted (${counts}).`;
|
|
33
|
+
}
|
|
34
|
+
export async function searchDocs(command, door) {
|
|
35
|
+
const answer = await askAgentDoor(door, {
|
|
36
|
+
path: docsSearchPath(command),
|
|
37
|
+
method: "GET",
|
|
38
|
+
label: "docs search",
|
|
39
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
40
|
+
});
|
|
41
|
+
if (!answer.ok)
|
|
42
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
43
|
+
const body = answer.body;
|
|
44
|
+
if (door.json)
|
|
45
|
+
return emitAgentDoor(door, body);
|
|
46
|
+
const results = body.results ?? [];
|
|
47
|
+
if (results.length === 0) {
|
|
48
|
+
writeLine(door.io.stdout, body.coverage === "exhausted" || body.coverage === undefined
|
|
49
|
+
? "Nothing you can read matches that."
|
|
50
|
+
: "Nothing in the pages searched matches that.");
|
|
51
|
+
}
|
|
52
|
+
for (const result of results) {
|
|
53
|
+
writeLine(door.io.stdout, `${result.title} (${result.slug ?? result.id})`);
|
|
54
|
+
const where = result.matched_in ? `matched in ${result.matched_in} · ` : "";
|
|
55
|
+
writeLine(door.io.stdout, ` ${where}${result.id} · updated ${result.updated_at}`);
|
|
56
|
+
if (result.snippet)
|
|
57
|
+
writeLine(door.io.stdout, ` ${result.snippet}`);
|
|
58
|
+
}
|
|
59
|
+
writeLine(door.io.stdout, "");
|
|
60
|
+
writeLine(door.io.stdout, `${body.returned ?? results.length}/${body.matched ?? results.length} matched/returned.`);
|
|
61
|
+
writeLine(door.io.stdout, docsSearchCoverageLine(body));
|
|
62
|
+
console.error(`${TAG} searched`, JSON.stringify({ matched: body.matched ?? 0, returned: results.length, coverage: body.coverage ?? "unknown" }));
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit docs sync-resolve|manifest` argument parsing (BLI-21875, docs parity step 12). `sync-status` keeps its parser in
|
|
3
|
+
* `local-args-tower-docs-msg.ts`; its single-page answer gained detail, not flags.
|
|
4
|
+
*
|
|
5
|
+
* sync-resolve <id|slug> --version <id> --keep-current|--use-kept [--expected-revision N] [--request-id U]
|
|
6
|
+
* manifest [--under <id|slug>] [--cursor C] [--all] [--out <file>]
|
|
7
|
+
*
|
|
8
|
+
* The door checks authority and the revision; this file only checks shape. It never picks a choice for the person.
|
|
9
|
+
*/
|
|
10
|
+
import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
11
|
+
const VALUE_FLAGS = ["--home", "--dashboard-url", "--version", "--expected-revision", "--request-id", "--under", "--cursor", "--out"];
|
|
12
|
+
const BOOLEAN_FLAGS = ["--json", "--keep-current", "--use-kept", "--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 isDocsSyncInvocation(args) {
|
|
15
|
+
return args[0] === "sync-resolve" || args[0] === "manifest";
|
|
16
|
+
}
|
|
17
|
+
export function parseDocsSyncArgs(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 === "manifest") {
|
|
37
|
+
only(["--under", "--cursor", "--all", "--out"]);
|
|
38
|
+
if (rest.length !== 0)
|
|
39
|
+
throw new Error("docs manifest takes no page; use --under <id|slug> for one subtree.");
|
|
40
|
+
const cursor = optionalNonEmpty(values.flags.get("--cursor"));
|
|
41
|
+
return {
|
|
42
|
+
...base,
|
|
43
|
+
action: "manifest",
|
|
44
|
+
...(cursor ? { cursor } : {}),
|
|
45
|
+
all: values.booleans.has("--all"),
|
|
46
|
+
outPath: optionalNonEmpty(values.flags.get("--out")),
|
|
47
|
+
docsSync: { under: optionalNonEmpty(values.flags.get("--under")) },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
only(["--version", "--keep-current", "--use-kept", "--expected-revision", "--request-id"]);
|
|
51
|
+
if (rest.length !== 1)
|
|
52
|
+
throw new Error("docs sync-resolve needs exactly one page id or slug, e.g. `cockpit docs sync-resolve runbook --version <id> --use-kept`.");
|
|
53
|
+
const keep = values.booleans.has("--keep-current");
|
|
54
|
+
const useKept = values.booleans.has("--use-kept");
|
|
55
|
+
if (keep === useKept) {
|
|
56
|
+
throw new Error("docs sync-resolve needs exactly one choice: --keep-current (keep the page's current text) or --use-kept (make the kept version current). Tower does not choose for you.");
|
|
57
|
+
}
|
|
58
|
+
const versionId = optionalNonEmpty(values.flags.get("--version"));
|
|
59
|
+
if (!versionId || !UUID.test(versionId))
|
|
60
|
+
throw new Error("docs sync-resolve needs --version <id>, the kept version's id (a UUID from `cockpit docs sync-status <page>`).");
|
|
61
|
+
const requestId = optionalNonEmpty(values.flags.get("--request-id"));
|
|
62
|
+
if (requestId && !UUID.test(requestId))
|
|
63
|
+
throw new Error("--request-id must be a UUID.");
|
|
64
|
+
const revisionRaw = optionalNonEmpty(values.flags.get("--expected-revision"));
|
|
65
|
+
let expectedRevision;
|
|
66
|
+
if (revisionRaw !== undefined) {
|
|
67
|
+
if (keep)
|
|
68
|
+
throw new Error("--expected-revision belongs with --use-kept: --keep-current writes no text.");
|
|
69
|
+
expectedRevision = /^\d+$/.test(revisionRaw) ? Number(revisionRaw) : Number.NaN;
|
|
70
|
+
if (!Number.isInteger(expectedRevision) || expectedRevision < 1)
|
|
71
|
+
throw new Error("--expected-revision is a positive whole number (the page revision you read).");
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
...base,
|
|
75
|
+
action: "sync-resolve",
|
|
76
|
+
docRef: rest[0],
|
|
77
|
+
versionId,
|
|
78
|
+
...(requestId ? { requestId } : {}),
|
|
79
|
+
...(expectedRevision !== undefined ? { expectedRevision } : {}),
|
|
80
|
+
docsSync: { choice: keep ? "keep_current" : "use_kept_version" },
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit docs sync-status <page>` detail, `sync-resolve` and `manifest` (BLI-21875): the Notion sync of one page and the
|
|
3
|
+
* library export manifest, over the same doors the browser calls.
|
|
4
|
+
*
|
|
5
|
+
* sync-status <page> GET /api/docs/documents/[id]/notion-sync
|
|
6
|
+
* sync-resolve <page> POST /api/docs/documents/[id]/notion-sync/resolve
|
|
7
|
+
* manifest GET /api/docs/manifest[?cursor=&under=]
|
|
8
|
+
*
|
|
9
|
+
* Every line about a receipt, a loss or a conflict is the door's own `sentence`, printed verbatim, so this terminal says
|
|
10
|
+
* what the web panel, the MCP tool and JARVIS say. Tower never asks Notion from a request: the answer is what the last
|
|
11
|
+
* passes recorded, with its age. `manifest` reports readiness; it gives no cutover verdict.
|
|
12
|
+
*/
|
|
13
|
+
import { open } from "node:fs/promises";
|
|
14
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
15
|
+
import { writeLine } from "./cli-io.js";
|
|
16
|
+
const TAG = "[docs sync cli]";
|
|
17
|
+
const READ_DEADLINE_MS = 60_000;
|
|
18
|
+
const WRITE_DEADLINE_MS = 60_000;
|
|
19
|
+
/** 100 calls of 2,000 pages: the most `manifest --all` walks before it says `capped`. */
|
|
20
|
+
const MANIFEST_MAX_CALLS = 100;
|
|
21
|
+
const list = (value) => (Array.isArray(value) ? value : []);
|
|
22
|
+
const str = (value) => (typeof value === "string" ? value : "");
|
|
23
|
+
const num = (value) => (typeof value === "number" ? value : 0);
|
|
24
|
+
const object = (value) => (typeof value === "object" && value !== null ? value : {});
|
|
25
|
+
export async function runDocsSync(command, door) {
|
|
26
|
+
switch (command.action) {
|
|
27
|
+
case "sync-resolve": return syncResolve(command, door);
|
|
28
|
+
case "manifest": return manifest(command, door);
|
|
29
|
+
default: return failAgentDoor(door, TAG, "invalid_body", "Try sync-status <page>, sync-resolve or manifest.");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** One page's sync status: the list row plus the receipts, losses, conflicts and decisions the door's `detail` carries. */
|
|
33
|
+
export function printSyncDetail(door, ref, body) {
|
|
34
|
+
const detail = object(body["detail"]);
|
|
35
|
+
const conflicts = object(detail["conflicts"]);
|
|
36
|
+
writeLine(door.io.stderr, `${TAG} status ${JSON.stringify({ conflicts: conflicts["total"] ?? null, open: conflicts["open"] ?? null, incomplete: list(detail["incomplete"]).length })}`);
|
|
37
|
+
const row = list(body["documents"])[0] ?? {};
|
|
38
|
+
writeLine(door.io.stdout, `${str(row["id"])} ${str(row["state"])} ${str(row["slug"]) || "(no slug)"} ${str(row["title"])}`);
|
|
39
|
+
writeLine(door.io.stdout, str(detail["state_sentence"]));
|
|
40
|
+
const receipts = object(detail["last_receipt"]);
|
|
41
|
+
for (const direction of ["push", "pull"]) {
|
|
42
|
+
const receipt = object(receipts[direction]);
|
|
43
|
+
writeLine(door.io.stdout, str(receipt["sentence"]) || `${direction === "push" ? "Push" : "Pull"}: no check recorded for this page yet.`);
|
|
44
|
+
}
|
|
45
|
+
const losses = object(detail["losses"]);
|
|
46
|
+
for (const line of Array.isArray(losses["sentences"]) ? losses["sentences"].map(str) : [])
|
|
47
|
+
writeLine(door.io.stdout, `Loss: ${line}`);
|
|
48
|
+
for (const name of Array.isArray(detail["incomplete"]) ? detail["incomplete"].map(str) : []) {
|
|
49
|
+
writeLine(door.io.stdout, `Could not read just now: ${name}`);
|
|
50
|
+
}
|
|
51
|
+
const items = list(conflicts["items"]);
|
|
52
|
+
for (const item of items) {
|
|
53
|
+
const resolution = object(item["resolution"]);
|
|
54
|
+
const status = str(item["status"]) === "open" ? "OPEN " : "resolved";
|
|
55
|
+
const decided = str(item["status"]) === "open" ? "" : ` decision: ${str(resolution["choice"])} at ${str(resolution["resolved_at"])}`;
|
|
56
|
+
writeLine(door.io.stdout, `Conflict ${status} version ${str(item["version_id"])} (v${num(item["version_number"])}, kept ${str(item["kept_at"])}) ${str(item["sentence"])}${decided}`);
|
|
57
|
+
}
|
|
58
|
+
writeLine(door.io.stdout, `${num(conflicts["total"])} conflict(s) kept, ${num(conflicts["open"])} open; showing the newest ${items.length}. As of ${str(detail["as_of"])}.`);
|
|
59
|
+
if (num(conflicts["open"]) > 0) {
|
|
60
|
+
writeLine(door.io.stdout, detail["can_edit"] === true
|
|
61
|
+
? `Resolve: cockpit docs sync-resolve ${ref} --version <version id> --keep-current | --use-kept [--expected-revision N]. ${str(detail["resolve_note"])}`
|
|
62
|
+
: "You can read this page but not edit it, so you cannot resolve its conflicts.");
|
|
63
|
+
}
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
async function syncResolve(command, door) {
|
|
67
|
+
const ref = command.docRef ?? "";
|
|
68
|
+
const answer = await askAgentDoor(door, {
|
|
69
|
+
path: `/api/docs/documents/${encodeURIComponent(ref)}/notion-sync/resolve`,
|
|
70
|
+
method: "POST",
|
|
71
|
+
label: "docs sync-resolve",
|
|
72
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
73
|
+
body: {
|
|
74
|
+
version_id: command.versionId,
|
|
75
|
+
choice: command.docsSync?.choice,
|
|
76
|
+
...(command.expectedRevision !== undefined ? { expected_revision: command.expectedRevision } : {}),
|
|
77
|
+
...(command.requestId ? { request_id: command.requestId } : {}),
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
if (!answer.ok)
|
|
81
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
82
|
+
const body = answer.body;
|
|
83
|
+
writeLine(door.io.stderr, `${TAG} resolve ${JSON.stringify({ outcome: body["outcome"] ?? null, choice: object(body["resolution"])["choice"] ?? null, body_written: body["body_written"] ?? null })}`);
|
|
84
|
+
if (door.json)
|
|
85
|
+
return emitAgentDoor(door, { ...body, ok: true });
|
|
86
|
+
const resolution = object(body["resolution"]);
|
|
87
|
+
writeLine(door.io.stdout, str(body["outcome"]) === "already_resolved"
|
|
88
|
+
? `Already resolved (${str(resolution["choice"])}, ${str(resolution["resolved_at"])}). Nothing was changed.`
|
|
89
|
+
: `Recorded ${str(resolution["choice"])} for version ${str(resolution["version_id"])}. Page revision now ${String(body["revision"] ?? "unknown")}${body["body_written"] === true ? "; the kept text is now current" : ""}.`);
|
|
90
|
+
writeLine(door.io.stdout, str(body["note"]));
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
async function manifest(command, door) {
|
|
94
|
+
const totals = { entries: [], seen: new Set(), requested: 0, returned: 0, failures: [] };
|
|
95
|
+
let cursor = command.cursor ?? null;
|
|
96
|
+
let calls = 0;
|
|
97
|
+
let status = "exhausted";
|
|
98
|
+
let reason;
|
|
99
|
+
let nextCursor = null;
|
|
100
|
+
let under = null;
|
|
101
|
+
let generatedAt = null;
|
|
102
|
+
for (;;) {
|
|
103
|
+
const query = new URLSearchParams();
|
|
104
|
+
if (cursor)
|
|
105
|
+
query.set("cursor", cursor);
|
|
106
|
+
if (command.docsSync?.under)
|
|
107
|
+
query.set("under", command.docsSync.under);
|
|
108
|
+
const answer = await askAgentDoor(door, {
|
|
109
|
+
path: `/api/docs/manifest${query.size > 0 ? `?${query.toString()}` : ""}`,
|
|
110
|
+
method: "GET",
|
|
111
|
+
label: "docs manifest",
|
|
112
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
113
|
+
});
|
|
114
|
+
if (!answer.ok) {
|
|
115
|
+
if (calls === 0)
|
|
116
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
117
|
+
// A later call failed: the pages already read are kept, and the cursor resumes after them.
|
|
118
|
+
status = "failed_partial";
|
|
119
|
+
reason = answer.reason;
|
|
120
|
+
nextCursor = cursor;
|
|
121
|
+
writeLine(door.io.stderr, `${TAG} manifest ${JSON.stringify({ call: calls + 1, reason: answer.reason, kept: totals.entries.length })}`);
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
calls += 1;
|
|
125
|
+
const page = answer.body;
|
|
126
|
+
const coverage = object(page["coverage"]);
|
|
127
|
+
totals.requested += num(page["requested"]);
|
|
128
|
+
totals.returned += num(page["returned"]);
|
|
129
|
+
for (const entry of list(page["entries"])) {
|
|
130
|
+
const id = str(entry["id"]);
|
|
131
|
+
if (!totals.seen.has(id)) {
|
|
132
|
+
totals.seen.add(id);
|
|
133
|
+
totals.entries.push(entry);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
totals.failures.push(...list(page["failures"]));
|
|
137
|
+
under = page["under"] ?? under;
|
|
138
|
+
generatedAt = page["generated_at"] ?? generatedAt;
|
|
139
|
+
nextCursor = typeof page["next_cursor"] === "string" ? page["next_cursor"] : null;
|
|
140
|
+
status = str(coverage["status"]) || "exhausted";
|
|
141
|
+
reason = str(coverage["reason"]) || undefined;
|
|
142
|
+
if (status === "failed_partial" || nextCursor === null || !command.all)
|
|
143
|
+
break;
|
|
144
|
+
if (calls >= MANIFEST_MAX_CALLS) {
|
|
145
|
+
status = "capped";
|
|
146
|
+
reason = "call_ceiling";
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
cursor = nextCursor;
|
|
150
|
+
}
|
|
151
|
+
const result = {
|
|
152
|
+
ok: true,
|
|
153
|
+
entries: totals.entries,
|
|
154
|
+
requested: totals.requested,
|
|
155
|
+
returned: totals.returned,
|
|
156
|
+
unique: totals.seen.size,
|
|
157
|
+
coverage: { status, calls, ...(reason ? { reason } : {}) },
|
|
158
|
+
coverage_text: `${totals.seen.size} unique pages in ${calls} call(s); requested ${totals.requested}, returned ${totals.returned}; read stopped: ${status}${reason ? ` (${reason})` : ""}.`,
|
|
159
|
+
failures: totals.failures,
|
|
160
|
+
next_cursor: status === "exhausted" ? null : nextCursor,
|
|
161
|
+
under,
|
|
162
|
+
generated_at: generatedAt,
|
|
163
|
+
};
|
|
164
|
+
writeLine(door.io.stderr, `${TAG} manifest ${JSON.stringify({ calls, requested: result.requested, returned: result.returned, unique: result.unique, status, failures: result.failures.length })}`);
|
|
165
|
+
if (command.outPath) {
|
|
166
|
+
try {
|
|
167
|
+
// "wx": never overwrite a file someone already has.
|
|
168
|
+
const handle = await open(command.outPath, "wx");
|
|
169
|
+
try {
|
|
170
|
+
await handle.writeFile(`${JSON.stringify(result, null, 2)}\n`);
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
await handle.close();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
const code = error.code;
|
|
178
|
+
return failAgentDoor(door, TAG, code === "EEXIST" ? "out_exists" : "out_write_failed", code === "EEXIST" ? `${command.outPath} already exists; choose another path or remove it.` : `Could not write ${command.outPath}.`);
|
|
179
|
+
}
|
|
180
|
+
writeLine(door.io.stderr, `${TAG} manifest written ${JSON.stringify({ entries: result.entries.length })}`);
|
|
181
|
+
}
|
|
182
|
+
if (door.json)
|
|
183
|
+
return emitAgentDoor(door, command.outPath ? { ...result, entries: undefined, written_to: command.outPath, entries_written: result.entries.length } : result, status === "failed_partial" ? 1 : 0);
|
|
184
|
+
for (const entry of totals.entries.slice(0, command.outPath ? 0 : 200)) {
|
|
185
|
+
const files = Array.isArray(entry["files"]) ? `${entry["files"].length} file(s)` : "files unknown";
|
|
186
|
+
writeLine(door.io.stdout, `${str(entry["id"])} r${num(entry["revision"])} ${str(entry["visibility"])} ${files} ${str(entry["slug"]) || "(no slug)"} ${str(entry["title"])}`);
|
|
187
|
+
}
|
|
188
|
+
if (!command.outPath && totals.entries.length > 200)
|
|
189
|
+
writeLine(door.io.stdout, `… ${totals.entries.length - 200} more; use --out <file> or --json for all of them.`);
|
|
190
|
+
writeLine(door.io.stdout, result.coverage_text);
|
|
191
|
+
for (const failure of totals.failures)
|
|
192
|
+
writeLine(door.io.stdout, `Failure: ${str(failure["stage"])} ${str(failure["reason"])} (${num(failure["count"])})`);
|
|
193
|
+
if (result.next_cursor)
|
|
194
|
+
writeLine(door.io.stdout, `More: cockpit docs manifest --cursor ${result.next_cursor}${command.docsSync?.under ? ` --under ${command.docsSync.under}` : ""}`);
|
|
195
|
+
writeLine(door.io.stdout, "This reports readiness only. It is not a cutover verdict.");
|
|
196
|
+
return status === "failed_partial" ? 1 : 0;
|
|
197
|
+
}
|