@bli-cockpit/cli 0.2.238 → 0.2.240
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/commands/cal-delete.js +13 -3
- package/dist/commands/cal-direct.js +119 -0
- package/dist/commands/cal-scope.js +71 -0
- package/dist/commands/cal.js +33 -2
- package/dist/commands/docs-share.js +81 -0
- package/dist/commands/docs.js +43 -6
- package/dist/commands/drills-render.js +2 -2
- package/dist/commands/drills.js +4 -4
- package/dist/commands/issue-list-page.js +101 -0
- package/dist/commands/issue-versions.js +93 -0
- package/dist/commands/issue.js +28 -5
- package/dist/commands/local-args-tower-cal.js +113 -9
- package/dist/commands/local-args-tower-docs-msg.js +52 -7
- package/dist/commands/local-args-tower-drills.js +1 -1
- package/dist/commands/local-args-tower-msg.js +62 -7
- package/dist/commands/local-args-tower-setup.js +11 -4
- package/dist/commands/local-args-tower-work.js +97 -11
- package/dist/commands/local-help-commands-tower-ops.js +3 -3
- package/dist/commands/local-help-commands-tower.js +27 -14
- package/dist/commands/local-help-commands.js +1 -1
- package/dist/commands/local-help.js +2 -2
- package/dist/commands/msg-read-line.js +32 -0
- package/dist/commands/msg-write.js +118 -0
- package/dist/commands/msg.js +64 -31
- package/dist/commands/project.js +15 -3
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/setup.js +13 -0
- package/dist/commands/usage.js +3 -1
- package/dist/spool/local-spool-parse.js +13 -12
- package/package.json +4 -4
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
2
2
|
import { writeLine } from "./cli-io.js";
|
|
3
|
+
import { writeScopeFacts } from "./cal-scope.js";
|
|
3
4
|
const TAG = "[cal cli]";
|
|
4
5
|
const DELETE_DEADLINE_MS = 60_000;
|
|
5
6
|
export async function runCalDelete(command, door) {
|
|
7
|
+
const query = new URLSearchParams();
|
|
8
|
+
if (command.accountName)
|
|
9
|
+
query.set("account", command.accountName);
|
|
10
|
+
if (command.scope)
|
|
11
|
+
query.set("scope", command.scope);
|
|
12
|
+
if (command.occurrenceStart)
|
|
13
|
+
query.set("occurrence_start", command.occurrenceStart);
|
|
6
14
|
const answer = await askAgentDoor(door, {
|
|
7
|
-
path: `/api/cal/events/${encodeURIComponent(command.subjectId ?? "")}${
|
|
15
|
+
path: `/api/cal/events/${encodeURIComponent(command.subjectId ?? "")}${query.size > 0 ? `?${query.toString()}` : ""}`,
|
|
8
16
|
method: "DELETE",
|
|
9
17
|
label: "cal delete",
|
|
10
18
|
timeoutMs: DELETE_DEADLINE_MS,
|
|
11
19
|
});
|
|
12
20
|
if (!answer.ok)
|
|
13
21
|
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
14
|
-
const
|
|
22
|
+
const full = answer.body;
|
|
23
|
+
const deleted = full.deleted;
|
|
15
24
|
if (door.json)
|
|
16
|
-
return emitAgentDoor(door, { ok: true,
|
|
25
|
+
return emitAgentDoor(door, { ok: true, ...full });
|
|
17
26
|
if (!deleted) {
|
|
18
27
|
writeLine(door.io.stdout, "Tower answered without a result. Nothing to report.");
|
|
19
28
|
return 0;
|
|
20
29
|
}
|
|
30
|
+
writeScopeFacts(door, full);
|
|
21
31
|
if (deleted.account_name)
|
|
22
32
|
writeLine(door.io.stdout, `Account: ${deleted.account_name}.`);
|
|
23
33
|
const alreadyGone = deleted.google === "already_gone_at_google";
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit cal duplicate | batch | undo` (BLI-21296, calendar slice 4): the
|
|
3
|
+
* terminal in front of the same doors the browser's grid uses. `batch` reads
|
|
4
|
+
* its changes as JSON on stdin; every verb takes `--idempotency-key` so a retry
|
|
5
|
+
* writes once, and `--json` prints the door's answer untouched.
|
|
6
|
+
*/
|
|
7
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
8
|
+
import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
|
|
9
|
+
const TAG = "[cal cli]";
|
|
10
|
+
const WRITE_DEADLINE_MS = 300_000;
|
|
11
|
+
export async function runCalDirect(command, door) {
|
|
12
|
+
if (command.action === "duplicate")
|
|
13
|
+
return duplicate(command, door);
|
|
14
|
+
if (command.action === "batch")
|
|
15
|
+
return batch(command, door);
|
|
16
|
+
return undo(command, door);
|
|
17
|
+
}
|
|
18
|
+
function keyed(command) {
|
|
19
|
+
return {
|
|
20
|
+
...(command.idempotencyKey ? { idempotency_key: command.idempotencyKey } : {}),
|
|
21
|
+
...(command.reason ? { reason: command.reason } : {}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function writeReceiptLines(door, receipt) {
|
|
25
|
+
if (receipt.event?.account_name)
|
|
26
|
+
writeLine(door.io.stdout, `Account: ${receipt.event.account_name}.`);
|
|
27
|
+
if (receipt.event?.event_id)
|
|
28
|
+
writeLine(door.io.stdout, `Tower event: ${receipt.event.event_id}`);
|
|
29
|
+
if (receipt.change_id)
|
|
30
|
+
writeLine(door.io.stdout, `Change: ${receipt.change_id} (undo with: cockpit cal undo ${receipt.change_id})`);
|
|
31
|
+
else
|
|
32
|
+
writeLine(door.io.stdout, "Google took the change but Tower could not record it in the ledger, so there is no change id to undo.");
|
|
33
|
+
if (receipt.event?.version)
|
|
34
|
+
writeLine(door.io.stdout, `Version: ${receipt.event.version}`);
|
|
35
|
+
if (receipt.replayed)
|
|
36
|
+
writeLine(door.io.stdout, "This key was used before: nothing new was written; this is the first receipt.");
|
|
37
|
+
}
|
|
38
|
+
async function duplicate(command, door) {
|
|
39
|
+
const answer = await askAgentDoor(door, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
path: `/api/cal/events/${encodeURIComponent(command.subjectId ?? "")}/duplicate`,
|
|
42
|
+
label: "cal duplicate",
|
|
43
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
44
|
+
body: {
|
|
45
|
+
...(command.startsAt ? { starts_at: command.startsAt } : {}),
|
|
46
|
+
...(command.endsAt ? { ends_at: command.endsAt } : {}),
|
|
47
|
+
...(command.calendarId ? { calendar_id: command.calendarId } : {}),
|
|
48
|
+
...(command.accountName ? { account: command.accountName } : {}),
|
|
49
|
+
...(command.keepInvitees ? { keep_invitees: true } : {}),
|
|
50
|
+
...keyed(command),
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
if (!answer.ok)
|
|
54
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
55
|
+
if (door.json)
|
|
56
|
+
return emitAgentDoor(door, { ok: true, ...answer.body });
|
|
57
|
+
const receipt = answer.body;
|
|
58
|
+
writeLine(door.io.stdout, `Duplicated "${receipt.event?.title ?? "the event"}". ${receipt.invitees === "kept" ? "The same guests were invited (Google emailed them)." : "No guests were invited."}`);
|
|
59
|
+
writeReceiptLines(door, receipt);
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
async function readBatchBody(door) {
|
|
63
|
+
if (isInteractiveStdin(door.io))
|
|
64
|
+
return null;
|
|
65
|
+
try {
|
|
66
|
+
const parsed = JSON.parse(await readPipedText(door.io.stdin, { maxChars: 200_000, overflowMessage: "The batch is too large." }));
|
|
67
|
+
if (Array.isArray(parsed))
|
|
68
|
+
return { changes: parsed };
|
|
69
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function batch(command, door) {
|
|
76
|
+
const body = await readBatchBody(door);
|
|
77
|
+
if (!body || !Array.isArray(body.changes)) {
|
|
78
|
+
return failAgentDoor(door, TAG, "invalid_body", 'Pipe JSON: {"changes":[{"event_id":"…","starts_at":"…","ends_at":"…","expected_version":"…"}]} (or a bare array). Up to 25 changes, each event once.');
|
|
79
|
+
}
|
|
80
|
+
const answer = await askAgentDoor(door, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
path: "/api/cal/events/batch",
|
|
83
|
+
label: "cal batch",
|
|
84
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
85
|
+
body: { ...body, ...keyed(command) },
|
|
86
|
+
});
|
|
87
|
+
if (!answer.ok)
|
|
88
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
89
|
+
const result = answer.body;
|
|
90
|
+
// A script must be able to tell "all of it" from "some of it" without parsing: exit 1 unless every item was written.
|
|
91
|
+
const code = result.outcome === "all_committed" ? 0 : 1;
|
|
92
|
+
if (door.json)
|
|
93
|
+
return emitAgentDoor(door, { ok: code === 0, ...result }, code);
|
|
94
|
+
for (const item of result.items) {
|
|
95
|
+
writeLine(door.io.stdout, `#${item.index + 1} ${item.event_id}: ${item.outcome}${item.reason ? ` (${item.reason})` : ""}${item.change_id ? ` change ${item.change_id}` : ""}`);
|
|
96
|
+
}
|
|
97
|
+
writeLine(door.io.stdout, `${result.committed} of ${result.items.length} changed (${result.outcome.replaceAll("_", " ")}).`);
|
|
98
|
+
if (result.unchanged.length > 0) {
|
|
99
|
+
writeLine(door.io.stdout, `Left as they were: ${result.unchanged.map((item) => `#${item.index + 1} ${item.event_id}`).join(", ")}.`);
|
|
100
|
+
}
|
|
101
|
+
return code;
|
|
102
|
+
}
|
|
103
|
+
async function undo(command, door) {
|
|
104
|
+
const answer = await askAgentDoor(door, {
|
|
105
|
+
method: "POST",
|
|
106
|
+
path: "/api/cal/undo",
|
|
107
|
+
label: "cal undo",
|
|
108
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
109
|
+
body: { ...(command.subjectId ? { change_id: command.subjectId } : {}), ...keyed(command) },
|
|
110
|
+
});
|
|
111
|
+
if (!answer.ok)
|
|
112
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
113
|
+
if (door.json)
|
|
114
|
+
return emitAgentDoor(door, { ok: true, ...answer.body });
|
|
115
|
+
const receipt = answer.body;
|
|
116
|
+
writeLine(door.io.stdout, `Undid change ${receipt.undoes_change_id}. The earlier values are back (or the created event is gone); guests, if any, were told by Google.`);
|
|
117
|
+
writeReceiptLines(door, receipt);
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recurring-event scope, RSVP and event-detail flags for `cockpit cal`
|
|
3
|
+
* (BLI-21311, slice 5). The scope words ("this event", "this and following
|
|
4
|
+
* events", "all events") come from the door and are printed as it sent them;
|
|
5
|
+
* this module only builds the request fields and says what came back.
|
|
6
|
+
*/
|
|
7
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
8
|
+
import { writeLine } from "./cli-io.js";
|
|
9
|
+
const TAG = "[cal cli]";
|
|
10
|
+
const WRITE_DEADLINE_MS = 60_000;
|
|
11
|
+
/** `scope` and `occurrence_start`, only when the caller gave them. */
|
|
12
|
+
export function scopeFields(command) {
|
|
13
|
+
return {
|
|
14
|
+
...(command.scope ? { scope: command.scope } : {}),
|
|
15
|
+
...(command.occurrenceStart ? { occurrence_start: command.occurrenceStart } : {}),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** `color_id`, `reminders`, `visibility`, `transparency`, only when given. */
|
|
19
|
+
export function detailFields(command) {
|
|
20
|
+
return {
|
|
21
|
+
...(command.colorId !== undefined ? { color_id: command.colorId } : {}),
|
|
22
|
+
...(command.reminders !== undefined ? { reminders: command.reminders } : {}),
|
|
23
|
+
...(command.visibility ? { visibility: command.visibility } : {}),
|
|
24
|
+
...(command.transparency ? { transparency: command.transparency } : {}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Prints the scope label, occurrence, split ids and notice the door answered with. */
|
|
28
|
+
export function writeScopeFacts(door, facts) {
|
|
29
|
+
if (facts.scope_label)
|
|
30
|
+
writeLine(door.io.stdout, `Applied to: ${facts.scope_label}${facts.occurrence_start ? ` (occurrence ${facts.occurrence_start})` : ""}.`);
|
|
31
|
+
if (facts.trimmed_until)
|
|
32
|
+
writeLine(door.io.stdout, `Series now ends: ${facts.trimmed_until}.`);
|
|
33
|
+
const split = facts.series_split;
|
|
34
|
+
if (split) {
|
|
35
|
+
writeLine(door.io.stdout, `New series: ${split.new_series_id ?? "unknown"}. Trimmed series: ${split.trimmed_series_id ?? "unknown"}${split.trimmed_until ? ` (until ${split.trimmed_until})` : ""}.`);
|
|
36
|
+
if (split.atomic === false)
|
|
37
|
+
writeLine(door.io.stdout, "The split ran in steps and is not atomic; check both series.");
|
|
38
|
+
}
|
|
39
|
+
if (facts.notice)
|
|
40
|
+
writeLine(door.io.stdout, facts.notice);
|
|
41
|
+
}
|
|
42
|
+
export async function runCalRsvp(command, door) {
|
|
43
|
+
const answer = await askAgentDoor(door, {
|
|
44
|
+
path: `/api/cal/events/${encodeURIComponent(command.subjectId ?? "")}/rsvp`,
|
|
45
|
+
method: "POST",
|
|
46
|
+
label: "cal rsvp",
|
|
47
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
48
|
+
body: {
|
|
49
|
+
response: command.rsvpResponse,
|
|
50
|
+
...(command.rsvpComment ? { comment: command.rsvpComment } : {}),
|
|
51
|
+
...(command.accountName ? { account: command.accountName } : {}),
|
|
52
|
+
...scopeFields(command),
|
|
53
|
+
...(command.expectedVersion ? { expected_version: command.expectedVersion } : {}),
|
|
54
|
+
...(command.idempotencyKey ? { idempotency_key: command.idempotencyKey } : {}),
|
|
55
|
+
...(command.reason ? { reason: command.reason } : {}),
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
if (!answer.ok)
|
|
59
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
60
|
+
if (door.json)
|
|
61
|
+
return emitAgentDoor(door, answer.body);
|
|
62
|
+
const body = answer.body;
|
|
63
|
+
const rsvp = body.rsvp;
|
|
64
|
+
writeLine(door.io.stdout, `RSVP ${rsvp?.response ?? command.rsvpResponse}${rsvp?.previous_response ? ` (was ${rsvp.previous_response})` : ""}${body.replayed ? " - already recorded, nothing sent again" : ""}.`);
|
|
65
|
+
writeScopeFacts(door, { scope_label: rsvp?.scope_label, occurrence_start: rsvp?.occurrence_start, notice: body.notice });
|
|
66
|
+
if (body.change_id)
|
|
67
|
+
writeLine(door.io.stdout, `Change: ${body.change_id}`);
|
|
68
|
+
if (body.version)
|
|
69
|
+
writeLine(door.io.stdout, `Version: ${body.version}`);
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
package/dist/commands/cal.js
CHANGED
|
@@ -25,6 +25,8 @@ import { runCalAddIcal, runCalDetach, runCalShare, runCalSync } from "./cal-cale
|
|
|
25
25
|
import { runCalDetail } from "./cal-detail.js";
|
|
26
26
|
import { runCalOverlay } from "./cal-overlay.js";
|
|
27
27
|
import { runCalDelete } from "./cal-delete.js";
|
|
28
|
+
import { detailFields, runCalRsvp, scopeFields, writeScopeFacts } from "./cal-scope.js";
|
|
29
|
+
import { runCalDirect } from "./cal-direct.js";
|
|
28
30
|
import { runCalSyncStatus } from "./cal-sync-status.js";
|
|
29
31
|
import { writeLine } from "./cli-io.js";
|
|
30
32
|
import { cacheAgeLabel, cacheCoverage, readThroughCache } from "./read-cache.js";
|
|
@@ -37,6 +39,10 @@ export async function runCal(command, io) {
|
|
|
37
39
|
case "event":
|
|
38
40
|
case "export":
|
|
39
41
|
return runCalDetail(command, door);
|
|
42
|
+
case "duplicate":
|
|
43
|
+
case "batch":
|
|
44
|
+
case "undo":
|
|
45
|
+
return runCalDirect(command, door);
|
|
40
46
|
case "conflicts":
|
|
41
47
|
case "calendar-change":
|
|
42
48
|
return runCalOverlay(command, door);
|
|
@@ -62,6 +68,8 @@ export async function runCal(command, io) {
|
|
|
62
68
|
return runCalSyncStatus(command, door);
|
|
63
69
|
case "change":
|
|
64
70
|
return changeEvent(command, door);
|
|
71
|
+
case "rsvp":
|
|
72
|
+
return runCalRsvp(command, door);
|
|
65
73
|
case "cancel":
|
|
66
74
|
case "delete":
|
|
67
75
|
return runCalDelete(command, door);
|
|
@@ -299,6 +307,9 @@ async function create(command, door) {
|
|
|
299
307
|
...(command.allDay ? { all_day: true } : {}),
|
|
300
308
|
...(command.attendees.length > 0 ? { attendees: command.attendees } : {}),
|
|
301
309
|
...(command.meet ? { meet: true } : {}),
|
|
310
|
+
...detailFields(command),
|
|
311
|
+
...(command.idempotencyKey ? { idempotency_key: command.idempotencyKey } : {}),
|
|
312
|
+
...(command.reason ? { reason: command.reason } : {}),
|
|
302
313
|
time_zone: command.timeZone ?? localTimeZone(),
|
|
303
314
|
},
|
|
304
315
|
});
|
|
@@ -340,11 +351,31 @@ function meetingLinkText(event) {
|
|
|
340
351
|
}
|
|
341
352
|
async function changeEvent(command, door) {
|
|
342
353
|
const answer = await askAgentDoor(door, { path: `/api/cal/events/${encodeURIComponent(command.subjectId)}`, method: "PATCH", label: "cal change", timeoutMs: WRITE_DEADLINE_MS,
|
|
343
|
-
body: {
|
|
354
|
+
body: {
|
|
355
|
+
...(command.accountName ? { account: command.accountName } : {}),
|
|
356
|
+
...(command.title ? { title: command.title } : {}),
|
|
357
|
+
...(command.startsAt ? { starts_at: command.startsAt } : {}),
|
|
358
|
+
...(command.endsAt ? { ends_at: command.endsAt } : {}),
|
|
359
|
+
...(command.location ? { location: command.location } : {}),
|
|
360
|
+
// BLI-21296: all-day conversion either way, and the write controls.
|
|
361
|
+
...(command.allDay ? { all_day: true } : command.timed ? { all_day: false } : {}),
|
|
362
|
+
...(command.timeZone && (command.allDay || command.timed) ? { time_zone: command.timeZone } : {}),
|
|
363
|
+
...scopeFields(command),
|
|
364
|
+
...detailFields(command),
|
|
365
|
+
...(command.expectedVersion ? { expected_version: command.expectedVersion } : {}),
|
|
366
|
+
...(command.idempotencyKey ? { idempotency_key: command.idempotencyKey } : {}),
|
|
367
|
+
...(command.reason ? { reason: command.reason } : {}),
|
|
368
|
+
} });
|
|
344
369
|
if (!answer.ok)
|
|
345
370
|
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
346
371
|
if (door.json)
|
|
347
372
|
return emitAgentDoor(door, { ok: true, ...answer.body });
|
|
348
|
-
|
|
373
|
+
const changed = answer.body;
|
|
374
|
+
writeLine(door.io.stdout, `Changed event in ${changed.event?.account_name ?? "its account"}.`);
|
|
375
|
+
writeScopeFacts(door, changed);
|
|
376
|
+
if (changed.change_id)
|
|
377
|
+
writeLine(door.io.stdout, `Change: ${changed.change_id} (undo with: cockpit cal undo ${changed.change_id})`);
|
|
378
|
+
if (changed.version)
|
|
379
|
+
writeLine(door.io.stdout, `Version: ${changed.version}`);
|
|
349
380
|
return 0;
|
|
350
381
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit docs share` and `cockpit docs access` — named page editors from a terminal (BLI-21313).
|
|
3
|
+
* A sibling of `docs.ts`, like `docs-notion.ts`.
|
|
4
|
+
*
|
|
5
|
+
* `access <id|slug>` → `GET /api/docs/documents/[id]/grants`
|
|
6
|
+
* `share <id|slug> --add <email>` → `POST /api/docs/documents/[id]/grants`
|
|
7
|
+
* `share <id|slug> --remove <email>` → `DELETE /api/docs/documents/[id]/grants`
|
|
8
|
+
*
|
|
9
|
+
* The door answers with the shared permission explanation, and every refusal keeps its own reason
|
|
10
|
+
* label verbatim (`private_page_grant_needs_decision`, `not_grant_manager`, `person_not_found`, ...).
|
|
11
|
+
* A person is an exact email; the server resolves it, so this file never guesses who someone is.
|
|
12
|
+
*/
|
|
13
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
14
|
+
import { writeLine } from "./cli-io.js";
|
|
15
|
+
import { resolveDocId } from "./docs.js";
|
|
16
|
+
const TAG = "[docs cli]";
|
|
17
|
+
const READ_DEADLINE_MS = 30_000;
|
|
18
|
+
const WRITE_DEADLINE_MS = 60_000;
|
|
19
|
+
async function resolveOrFail(command, door) {
|
|
20
|
+
const ref = command.docRef ?? "";
|
|
21
|
+
const resolved = await resolveDocId(door, ref);
|
|
22
|
+
if (resolved.status === "list_failed")
|
|
23
|
+
return { exit: failAgentDoor(door, TAG, resolved.reason, resolved.detail) };
|
|
24
|
+
if (resolved.status === "not_found") {
|
|
25
|
+
return { exit: failAgentDoor(door, TAG, "document_not_found_or_unreadable", `That document does not exist, or you cannot read it: ${ref}`) };
|
|
26
|
+
}
|
|
27
|
+
return { id: resolved.id };
|
|
28
|
+
}
|
|
29
|
+
function printPermission(door, permission) {
|
|
30
|
+
for (const line of permission.reasons)
|
|
31
|
+
writeLine(door.io.stdout, line);
|
|
32
|
+
for (const editor of permission.editors)
|
|
33
|
+
writeLine(door.io.stdout, ` editor: ${editor.display_name}${editor.email ? ` <${editor.email}>` : ""}`);
|
|
34
|
+
}
|
|
35
|
+
export async function accessDocs(command, door) {
|
|
36
|
+
const target = await resolveOrFail(command, door);
|
|
37
|
+
if ("exit" in target)
|
|
38
|
+
return target.exit;
|
|
39
|
+
const answer = await askAgentDoor(door, {
|
|
40
|
+
path: `/api/docs/documents/${encodeURIComponent(target.id)}/grants`,
|
|
41
|
+
method: "GET",
|
|
42
|
+
label: "docs access",
|
|
43
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
44
|
+
});
|
|
45
|
+
if (!answer.ok)
|
|
46
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
47
|
+
const body = answer.body;
|
|
48
|
+
writeLine(door.io.stderr, `${TAG} access ${JSON.stringify({ document_id: target.id, role: body.permission.role, editors: body.permission.editors.length })}`);
|
|
49
|
+
if (door.json)
|
|
50
|
+
return emitAgentDoor(door, { ok: true, ...body });
|
|
51
|
+
printPermission(door, body.permission);
|
|
52
|
+
for (const event of body.events ?? []) {
|
|
53
|
+
writeLine(door.io.stdout, ` ${event.created_at}: ${event.actor_name} ${event.action === "granted" ? "added" : "removed"} ${event.grantee_name}`);
|
|
54
|
+
}
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
export async function shareDocs(command, door) {
|
|
58
|
+
const target = await resolveOrFail(command, door);
|
|
59
|
+
if ("exit" in target)
|
|
60
|
+
return target.exit;
|
|
61
|
+
const adding = command.addPerson !== undefined;
|
|
62
|
+
const answer = await askAgentDoor(door, {
|
|
63
|
+
path: `/api/docs/documents/${encodeURIComponent(target.id)}/grants`,
|
|
64
|
+
method: adding ? "POST" : "DELETE",
|
|
65
|
+
label: adding ? "docs share add" : "docs share remove",
|
|
66
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
67
|
+
body: { person: adding ? command.addPerson : command.removePerson, ...(command.reason ? { reason: command.reason } : {}) },
|
|
68
|
+
});
|
|
69
|
+
if (!answer.ok)
|
|
70
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
71
|
+
const body = answer.body;
|
|
72
|
+
writeLine(door.io.stderr, `${TAG} share ${JSON.stringify({ document_id: target.id, action: adding ? "add" : "remove", outcome: body.outcome, reason: body.reason, request_id: body.request_id })}`);
|
|
73
|
+
if (door.json)
|
|
74
|
+
return emitAgentDoor(door, { ok: true, ...body });
|
|
75
|
+
const person = adding ? command.addPerson : command.removePerson;
|
|
76
|
+
writeLine(door.io.stdout, body.outcome === "unchanged"
|
|
77
|
+
? `Nothing changed: ${person} ${adding ? "is already an editor" : "was not an editor"}.`
|
|
78
|
+
: `${adding ? "Added" : "Removed"} ${person} ${adding ? "as an editor of" : "from the editors of"} ${target.id}.`);
|
|
79
|
+
printPermission(door, body.permission);
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
package/dist/commands/docs.js
CHANGED
|
@@ -27,11 +27,13 @@
|
|
|
27
27
|
* `document_not_writable`, `title_too_long`, `content_too_long`,
|
|
28
28
|
* `circular_parent`, and so on — never rephrased here.
|
|
29
29
|
*/
|
|
30
|
+
import { randomUUID } from "node:crypto";
|
|
30
31
|
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
|
|
31
32
|
import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
|
|
32
33
|
import { readDoc } from "./docs-read.js";
|
|
33
34
|
import { exportDoc } from "./docs-export.js";
|
|
34
35
|
import { linkDoc, syncStatusDocs } from "./docs-notion.js";
|
|
36
|
+
import { accessDocs, shareDocs } from "./docs-share.js";
|
|
35
37
|
import { readNoteFile } from "./notes-file.js";
|
|
36
38
|
import { cacheAgeLabel, cacheCoverage, readThroughCache } from "./read-cache.js";
|
|
37
39
|
const TAG = "[docs cli]";
|
|
@@ -59,6 +61,12 @@ export async function runDocs(command, io) {
|
|
|
59
61
|
return syncStatusDocs(command, door);
|
|
60
62
|
case "export":
|
|
61
63
|
return exportDoc(command, door);
|
|
64
|
+
case "mutation-status":
|
|
65
|
+
return mutationStatusDocs(command, door);
|
|
66
|
+
case "share":
|
|
67
|
+
return shareDocs(command, door);
|
|
68
|
+
case "access":
|
|
69
|
+
return accessDocs(command, door);
|
|
62
70
|
}
|
|
63
71
|
}
|
|
64
72
|
/** `--parent`, `--query`, `--word`, `--limit`, `--bodies` (BLI-10450) as the door's own query parameters (BLI-3737). */
|
|
@@ -248,14 +256,41 @@ async function updateDoc(command, door) {
|
|
|
248
256
|
&& !command.clearParent) {
|
|
249
257
|
return failAgentDoor(door, TAG, "invalid_body", "docs update needs at least one of --title, --visibility, --parent/--clear-parent, or a body on --body-stdin/--file.");
|
|
250
258
|
}
|
|
251
|
-
|
|
259
|
+
// BLI-21298: every save carries a request id, so a lost response can be checked and a retry applies once.
|
|
260
|
+
const requestId = command.requestId ?? randomUUID();
|
|
261
|
+
writeLine(door.io.stderr, `${TAG} request ${JSON.stringify({ request_id: requestId, expected_revision: command.expectedRevision })}`);
|
|
262
|
+
const answer = await updateDocument(command, door, resolved.id, bodyMarkdown, requestId);
|
|
263
|
+
if (!answer.ok)
|
|
264
|
+
return refuseDocsWrite(door, answer, requestId);
|
|
265
|
+
const { document = {}, mutation } = answer.body;
|
|
266
|
+
writeLine(door.io.stderr, `${TAG} updated ${JSON.stringify({ document_id: document["id"] ?? resolved.id, request_id: requestId, revision: mutation?.["revision"] ?? document["revision"] })}`);
|
|
267
|
+
if (door.json)
|
|
268
|
+
return emitAgentDoor(door, { ok: true, document, mutation });
|
|
269
|
+
writeLine(door.io.stdout, `Updated ${String(document["title"] ?? "")} (${String(document["id"] ?? resolved.id)}), revision ${String(mutation?.["revision"] ?? document["revision"] ?? "?")}.`);
|
|
270
|
+
return 0;
|
|
271
|
+
}
|
|
272
|
+
/** Says where the save stands, then fails with the door's own reason (a conflict names the current revision). */
|
|
273
|
+
function refuseDocsWrite(door, answer, requestId) {
|
|
274
|
+
const mutation = answer.body?.mutation;
|
|
275
|
+
writeLine(door.io.stderr, `${TAG} check the save with cockpit docs mutation-status ${requestId}; retry unchanged input with --request-id ${requestId}.`);
|
|
276
|
+
if (mutation?.current) {
|
|
277
|
+
writeLine(door.io.stderr, `${TAG} conflict ${JSON.stringify({ request_id: requestId, current_revision: mutation.current.revision ?? mutation.revision })}`);
|
|
278
|
+
}
|
|
279
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail, mutation ? { mutation } : {});
|
|
280
|
+
}
|
|
281
|
+
/** BLI-21298: reads only your own save receipt. A missing receipt is unknown_check_again, never "nothing changed". */
|
|
282
|
+
async function mutationStatusDocs(command, door) {
|
|
283
|
+
const answer = await askAgentDoor(door, {
|
|
284
|
+
path: `/api/docs/mutations/${encodeURIComponent(command.docRef ?? "")}`,
|
|
285
|
+
method: "GET",
|
|
286
|
+
label: "docs mutation status",
|
|
287
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
288
|
+
});
|
|
252
289
|
if (!answer.ok)
|
|
253
290
|
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
254
|
-
const document = answer.body.document ?? {};
|
|
255
|
-
writeLine(door.io.stderr, `${TAG} updated ${JSON.stringify({ document_id: document["id"] ?? resolved.id })}`);
|
|
256
291
|
if (door.json)
|
|
257
|
-
return emitAgentDoor(door, { ok: true,
|
|
258
|
-
writeLine(door.io.stdout,
|
|
292
|
+
return emitAgentDoor(door, { ok: true, ...answer.body });
|
|
293
|
+
writeLine(door.io.stdout, JSON.stringify(answer.body));
|
|
259
294
|
return 0;
|
|
260
295
|
}
|
|
261
296
|
/**
|
|
@@ -327,13 +362,15 @@ async function deleteOneDocument(door, ref, id) {
|
|
|
327
362
|
return { ref, ok: false, reason: answer.reason, detail: answer.detail };
|
|
328
363
|
return { ref, ok: true, id };
|
|
329
364
|
}
|
|
330
|
-
async function updateDocument(command, door, documentId, bodyMarkdown) {
|
|
365
|
+
async function updateDocument(command, door, documentId, bodyMarkdown, requestId) {
|
|
331
366
|
return askAgentDoor(door, {
|
|
332
367
|
path: `/api/docs/documents/${encodeURIComponent(documentId)}`,
|
|
333
368
|
method: "PATCH",
|
|
334
369
|
label: "docs update",
|
|
335
370
|
timeoutMs: WRITE_DEADLINE_MS,
|
|
336
371
|
body: {
|
|
372
|
+
request_id: requestId,
|
|
373
|
+
...(command.expectedRevision !== undefined ? { expected_revision: command.expectedRevision } : {}),
|
|
337
374
|
...(command.title !== undefined ? { title: command.title } : {}),
|
|
338
375
|
...(bodyMarkdown !== undefined ? { body_markdown: bodyMarkdown } : {}),
|
|
339
376
|
...(command.visibility !== undefined ? { visibility: command.visibility } : {}),
|
|
@@ -21,9 +21,9 @@ export function overviewLines(body) {
|
|
|
21
21
|
lines.push(`${body.queue.newLeft} new drill${body.queue.newLeft === 1 ? "" : "s"} waiting. Run: cockpit drills start`, "");
|
|
22
22
|
else
|
|
23
23
|
lines.push("You are all caught up. Nothing new is waiting.", "");
|
|
24
|
-
lines.push(
|
|
24
|
+
lines.push(body.theme.plain ? "Theme: Plain (no theme; if you like, pick one with `cockpit drills theme set <theme>`)" : `Theme: ${body.theme.label}`, "");
|
|
25
25
|
if (body.bosses.length > 0) {
|
|
26
|
-
lines.push("Bosses");
|
|
26
|
+
lines.push(body.theme.plain ? "Mistakes you are working on" : "Bosses");
|
|
27
27
|
for (const boss of body.bosses)
|
|
28
28
|
lines.push(` ${boss.bossName}: ${boss.progressLine}`);
|
|
29
29
|
lines.push("");
|
package/dist/commands/drills.js
CHANGED
|
@@ -114,11 +114,11 @@ async function themeShow(door) {
|
|
|
114
114
|
writeLine(door.io.stderr, `${TAG} theme answered ${JSON.stringify({ key: body.theme.key, chosen: body.theme.chosen, ready: body.ready })}`);
|
|
115
115
|
if (door.json)
|
|
116
116
|
return emitAgentDoor(door, response.body);
|
|
117
|
-
writeLine(door.io.stdout, `Your theme: ${body.theme.label}${body.theme.chosen ? "" : " (the default)"}`);
|
|
118
|
-
writeLine(door.io.stdout, "
|
|
117
|
+
writeLine(door.io.stdout, `Your theme: ${body.theme.label}${body.theme.chosen ? "" : " (the default: drills are shown straight, with no theme)"}`);
|
|
118
|
+
writeLine(door.io.stdout, "If you like, pick a theme (plain turns it off):");
|
|
119
119
|
for (const choice of body.themes)
|
|
120
|
-
writeLine(door.io.stdout, ` ${choice.key}: ${choice.blurb}${choice.key === body.suggestedTheme && !body.theme.chosen ? " (suggested)" : ""}`);
|
|
121
|
-
writeLine(door.io.stdout, "Or
|
|
120
|
+
writeLine(door.io.stdout, ` ${choice.key}: ${choice.blurb}${choice.key === body.suggestedTheme && choice.key !== "plain" && !body.theme.chosen ? " (suggested)" : ""}`);
|
|
121
|
+
writeLine(door.io.stdout, "Or type anything you're into. Change it with: cockpit drills theme set <theme> [--play <what you play>] [--watch <what you watch>] [--train <what you train>]");
|
|
122
122
|
return 0;
|
|
123
123
|
}
|
|
124
124
|
async function themeSet(command, door) {
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cursor contract on the CLI (BLI-21315): the page fields a list prints,
|
|
3
|
+
* the extra query a page carries, and `--all`, which walks every page and
|
|
4
|
+
* streams one JSON object per line so a large export never buffers.
|
|
5
|
+
*
|
|
6
|
+
* The door (`GET /api/work/issues`) owns the filters, the order and the
|
|
7
|
+
* cursor; this file only carries them over and reports what came back. A
|
|
8
|
+
* walk that stops early still ends with a final line naming the coverage and
|
|
9
|
+
* the reason, and the command exits non-zero.
|
|
10
|
+
*/
|
|
11
|
+
import { askAgentDoor } from "./agent-door.js";
|
|
12
|
+
import { writeLine } from "./cli-io.js";
|
|
13
|
+
import { READ_DEADLINE_MS, TAG, withSizeLabel } from "./issue-contracts.js";
|
|
14
|
+
const ALL_PAGE_SIZE = 500;
|
|
15
|
+
/** The additive page fields as `--json` output, only the ones the door sent. */
|
|
16
|
+
export function pageFieldsOf(body) {
|
|
17
|
+
const fields = {};
|
|
18
|
+
for (const key of ["next_cursor", "has_more", "coverage", "requested", "returned", "unique", "failure_reason"]) {
|
|
19
|
+
if (body[key] !== undefined)
|
|
20
|
+
fields[key] = body[key];
|
|
21
|
+
}
|
|
22
|
+
return fields;
|
|
23
|
+
}
|
|
24
|
+
/** The sentence under a text page; null when the door sent no page fields or the page was whole. */
|
|
25
|
+
export function pageFooter(body, what) {
|
|
26
|
+
if (body.coverage === undefined)
|
|
27
|
+
return null;
|
|
28
|
+
if (body.coverage === "complete")
|
|
29
|
+
return null;
|
|
30
|
+
return body.next_cursor
|
|
31
|
+
? `More ${what} available (${body.returned ?? "?"} shown). Continue with --cursor ${body.next_cursor}, or use --all.`
|
|
32
|
+
: `More ${what} may exist; this Tower gave no cursor to continue from.`;
|
|
33
|
+
}
|
|
34
|
+
/** Filters, sort and cursor beyond the long-standing ones, as door query parameters. */
|
|
35
|
+
export function addIssuePageQuery(query, command) {
|
|
36
|
+
const pairs = [
|
|
37
|
+
["label", command.label],
|
|
38
|
+
["q", command.q],
|
|
39
|
+
["created_from", command.createdAfter],
|
|
40
|
+
["created_to", command.createdBefore],
|
|
41
|
+
["updated_from", command.updatedAfter],
|
|
42
|
+
["updated_to", command.updatedBefore],
|
|
43
|
+
["sort", command.sort],
|
|
44
|
+
["dir", command.dir],
|
|
45
|
+
["cursor", command.cursor],
|
|
46
|
+
];
|
|
47
|
+
for (const [name, value] of pairs)
|
|
48
|
+
if (value)
|
|
49
|
+
query.set(name, value);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* `list --all`: NDJSON on stdout, one issue per line, then ONE final line
|
|
53
|
+
* `{"type":"coverage",...}`. Exit 0 only when the walk was complete.
|
|
54
|
+
*/
|
|
55
|
+
export async function streamAllIssues(command, door, base) {
|
|
56
|
+
const query = new URLSearchParams(base);
|
|
57
|
+
query.set("limit", String(Math.min(command.limit ?? ALL_PAGE_SIZE, ALL_PAGE_SIZE)));
|
|
58
|
+
query.delete("cursor");
|
|
59
|
+
const seen = new Set();
|
|
60
|
+
let returned = 0;
|
|
61
|
+
let pages = 0;
|
|
62
|
+
let expected = null;
|
|
63
|
+
let cursor = null;
|
|
64
|
+
for (;;) {
|
|
65
|
+
if (cursor)
|
|
66
|
+
query.set("cursor", cursor);
|
|
67
|
+
const answer = await askAgentDoor(door, {
|
|
68
|
+
path: `/api/work/issues?${query.toString()}`,
|
|
69
|
+
method: "GET",
|
|
70
|
+
label: "issue list",
|
|
71
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
72
|
+
});
|
|
73
|
+
if (!answer.ok) {
|
|
74
|
+
console.error(`${TAG} walk interrupted`, JSON.stringify({ reason: answer.reason, pages, returned }));
|
|
75
|
+
writeLine(door.io.stdout, JSON.stringify({
|
|
76
|
+
type: "coverage", coverage: "failed_partial", reason: answer.reason, detail: answer.detail,
|
|
77
|
+
pages, returned, unique: seen.size, expected, next_cursor: cursor,
|
|
78
|
+
}));
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
const body = answer.body;
|
|
82
|
+
pages += 1;
|
|
83
|
+
if (pages === 1 && typeof body.counts?.total === "number")
|
|
84
|
+
expected = body.counts.total;
|
|
85
|
+
for (const issue of body.issues ?? []) {
|
|
86
|
+
returned += 1;
|
|
87
|
+
seen.add(issue.id);
|
|
88
|
+
writeLine(door.io.stdout, JSON.stringify(withSizeLabel(issue)));
|
|
89
|
+
}
|
|
90
|
+
console.error(`${TAG} page ${JSON.stringify({ page: pages, returned: body.issues?.length ?? 0, coverage: body.coverage ?? null })}`);
|
|
91
|
+
cursor = body.next_cursor ?? null;
|
|
92
|
+
if (!cursor)
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
const exact = expected === null || expected === returned;
|
|
96
|
+
writeLine(door.io.stdout, JSON.stringify({
|
|
97
|
+
type: "coverage", coverage: "complete", pages, returned, unique: seen.size, expected,
|
|
98
|
+
...(returned === seen.size && exact ? {} : { note: "returned, unique and expected do not all agree; rows changed during the walk" }),
|
|
99
|
+
}));
|
|
100
|
+
return 0;
|
|
101
|
+
}
|