@bli-cockpit/cli 0.2.239 → 0.2.241
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-scope.js +71 -0
- package/dist/commands/cal-view-settings.js +61 -0
- package/dist/commands/cal.js +11 -0
- package/dist/commands/docs-share.js +81 -0
- package/dist/commands/docs.js +5 -0
- 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.js +23 -5
- package/dist/commands/local-arg-values.js +90 -19
- package/dist/commands/local-args-tower-cal.js +148 -8
- package/dist/commands/local-args-tower-docs-msg.js +30 -3
- package/dist/commands/local-args-tower-drills.js +1 -1
- package/dist/commands/local-args-tower-manager.js +128 -28
- package/dist/commands/local-args-tower-msg.js +43 -6
- package/dist/commands/local-args-tower-work.js +74 -6
- package/dist/commands/local-help-commands-tower-ops.js +35 -21
- package/dist/commands/local-help-commands-tower.js +15 -11
- package/dist/commands/local-help.js +1 -1
- package/dist/commands/manager-fields-file.js +208 -0
- package/dist/commands/manager-hints.js +88 -0
- package/dist/commands/manager-limits.json +269 -0
- package/dist/commands/manager-read-reasons.js +78 -0
- package/dist/commands/manager-render-home.js +213 -37
- package/dist/commands/manager-render.js +119 -21
- package/dist/commands/manager-requests.js +7 -3
- package/dist/commands/manager.js +67 -43
- package/dist/commands/msg-write.js +118 -0
- package/dist/commands/msg.js +20 -29
- package/dist/commands/project.js +15 -3
- package/dist/commands/public-root.js +1 -1
- package/package.json +2 -2
|
@@ -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,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
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
2
|
+
import { readPipedText, isInteractiveStdin, writeLine } from "./cli-io.js";
|
|
3
|
+
const FAIL_TAG = "[cal cli]";
|
|
4
|
+
const REQUEST_TIMEOUT_MS = 30000;
|
|
5
|
+
const PATH = "/api/cal/view-settings";
|
|
6
|
+
/** `cal view-settings` (read) and `cal view-settings-change` (partial patch). Same door the browser uses. */
|
|
7
|
+
export async function runCalViewSettings(command, door) {
|
|
8
|
+
if (command.action === "view-settings-change")
|
|
9
|
+
return change(command, door);
|
|
10
|
+
const answer = await askAgentDoor(door, { method: "GET", path: PATH, label: "cal view-settings", timeoutMs: REQUEST_TIMEOUT_MS });
|
|
11
|
+
if (!answer.ok)
|
|
12
|
+
return failAgentDoor(door, FAIL_TAG, answer.reason, answer.detail);
|
|
13
|
+
if (door.json)
|
|
14
|
+
return emitAgentDoor(door, answer.body);
|
|
15
|
+
writeSettings(door, answer.body, true);
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
async function change(command, door) {
|
|
19
|
+
let body = command.viewSettings ?? {};
|
|
20
|
+
if (!Object.keys(body).length && !isInteractiveStdin(door.io)) {
|
|
21
|
+
const piped = await readPipedSettings(door);
|
|
22
|
+
if (piped === null)
|
|
23
|
+
return failAgentDoor(door, FAIL_TAG, "invalid_body", "Pipe a JSON object of settings, or use --view, --days, --week-start, --hide-weekends, --density, --zone.");
|
|
24
|
+
body = piped;
|
|
25
|
+
}
|
|
26
|
+
if (!Object.keys(body).length) {
|
|
27
|
+
return failAgentDoor(door, FAIL_TAG, "invalid_body", "Name a setting: --view month, --days 4, --week-start sunday, --hide-weekends on, --density compact, --zone America/Toronto (or --zone none).");
|
|
28
|
+
}
|
|
29
|
+
const answer = await askAgentDoor(door, { method: "PATCH", path: PATH, body, label: "cal view-settings-change", timeoutMs: REQUEST_TIMEOUT_MS });
|
|
30
|
+
if (!answer.ok)
|
|
31
|
+
return failAgentDoor(door, FAIL_TAG, answer.reason, answer.detail);
|
|
32
|
+
if (door.json)
|
|
33
|
+
return emitAgentDoor(door, answer.body);
|
|
34
|
+
writeLine(door.io.stdout, "View settings saved.");
|
|
35
|
+
writeSettings(door, answer.body, false);
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
function writeSettings(door, settings, withActions) {
|
|
39
|
+
const out = door.io.stdout;
|
|
40
|
+
writeLine(out, settings.saved ? `Saved ${settings.updated_at ?? ""}`.trim() : "Not saved yet: these are the defaults.");
|
|
41
|
+
writeLine(out, `default view: ${settings.default_view}${settings.default_view === "days" ? ` (${settings.day_count} days)` : ` · N days: ${settings.day_count}`}`);
|
|
42
|
+
writeLine(out, `week starts: ${settings.week_start} · weekends: ${settings.hide_weekends ? "hidden in week and N-day views" : "shown"} · density: ${settings.density}`);
|
|
43
|
+
writeLine(out, `extra time zones: ${settings.extra_zones.length ? settings.extra_zones.join(", ") : "none"}`);
|
|
44
|
+
if (withActions) {
|
|
45
|
+
writeLine(out, "Keyboard shortcuts:");
|
|
46
|
+
for (const action of settings.actions ?? [])
|
|
47
|
+
writeLine(out, ` ${action.keys.join(" ")} ${action.label}: ${action.description}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Piped JSON settings, or null when the pipe is unreadable or not a JSON object. */
|
|
51
|
+
async function readPipedSettings(door) {
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(await readPipedText(door.io.stdin, { maxChars: 4000, overflowMessage: "Settings are too large." }));
|
|
54
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
55
|
+
return null;
|
|
56
|
+
return parsed;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
package/dist/commands/cal.js
CHANGED
|
@@ -24,7 +24,9 @@ import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./ag
|
|
|
24
24
|
import { runCalAddIcal, runCalDetach, runCalShare, runCalSync } from "./cal-calendars-write.js";
|
|
25
25
|
import { runCalDetail } from "./cal-detail.js";
|
|
26
26
|
import { runCalOverlay } from "./cal-overlay.js";
|
|
27
|
+
import { runCalViewSettings } from "./cal-view-settings.js";
|
|
27
28
|
import { runCalDelete } from "./cal-delete.js";
|
|
29
|
+
import { detailFields, runCalRsvp, scopeFields, writeScopeFacts } from "./cal-scope.js";
|
|
28
30
|
import { runCalDirect } from "./cal-direct.js";
|
|
29
31
|
import { runCalSyncStatus } from "./cal-sync-status.js";
|
|
30
32
|
import { writeLine } from "./cli-io.js";
|
|
@@ -45,6 +47,9 @@ export async function runCal(command, io) {
|
|
|
45
47
|
case "conflicts":
|
|
46
48
|
case "calendar-change":
|
|
47
49
|
return runCalOverlay(command, door);
|
|
50
|
+
case "view-settings":
|
|
51
|
+
case "view-settings-change":
|
|
52
|
+
return runCalViewSettings(command, door);
|
|
48
53
|
case "today":
|
|
49
54
|
case "week":
|
|
50
55
|
case "next":
|
|
@@ -67,6 +72,8 @@ export async function runCal(command, io) {
|
|
|
67
72
|
return runCalSyncStatus(command, door);
|
|
68
73
|
case "change":
|
|
69
74
|
return changeEvent(command, door);
|
|
75
|
+
case "rsvp":
|
|
76
|
+
return runCalRsvp(command, door);
|
|
70
77
|
case "cancel":
|
|
71
78
|
case "delete":
|
|
72
79
|
return runCalDelete(command, door);
|
|
@@ -304,6 +311,7 @@ async function create(command, door) {
|
|
|
304
311
|
...(command.allDay ? { all_day: true } : {}),
|
|
305
312
|
...(command.attendees.length > 0 ? { attendees: command.attendees } : {}),
|
|
306
313
|
...(command.meet ? { meet: true } : {}),
|
|
314
|
+
...detailFields(command),
|
|
307
315
|
...(command.idempotencyKey ? { idempotency_key: command.idempotencyKey } : {}),
|
|
308
316
|
...(command.reason ? { reason: command.reason } : {}),
|
|
309
317
|
time_zone: command.timeZone ?? localTimeZone(),
|
|
@@ -356,6 +364,8 @@ async function changeEvent(command, door) {
|
|
|
356
364
|
// BLI-21296: all-day conversion either way, and the write controls.
|
|
357
365
|
...(command.allDay ? { all_day: true } : command.timed ? { all_day: false } : {}),
|
|
358
366
|
...(command.timeZone && (command.allDay || command.timed) ? { time_zone: command.timeZone } : {}),
|
|
367
|
+
...scopeFields(command),
|
|
368
|
+
...detailFields(command),
|
|
359
369
|
...(command.expectedVersion ? { expected_version: command.expectedVersion } : {}),
|
|
360
370
|
...(command.idempotencyKey ? { idempotency_key: command.idempotencyKey } : {}),
|
|
361
371
|
...(command.reason ? { reason: command.reason } : {}),
|
|
@@ -366,6 +376,7 @@ async function changeEvent(command, door) {
|
|
|
366
376
|
return emitAgentDoor(door, { ok: true, ...answer.body });
|
|
367
377
|
const changed = answer.body;
|
|
368
378
|
writeLine(door.io.stdout, `Changed event in ${changed.event?.account_name ?? "its account"}.`);
|
|
379
|
+
writeScopeFacts(door, changed);
|
|
369
380
|
if (changed.change_id)
|
|
370
381
|
writeLine(door.io.stdout, `Change: ${changed.change_id} (undo with: cockpit cal undo ${changed.change_id})`);
|
|
371
382
|
if (changed.version)
|
|
@@ -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
|
@@ -33,6 +33,7 @@ import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
|
|
|
33
33
|
import { readDoc } from "./docs-read.js";
|
|
34
34
|
import { exportDoc } from "./docs-export.js";
|
|
35
35
|
import { linkDoc, syncStatusDocs } from "./docs-notion.js";
|
|
36
|
+
import { accessDocs, shareDocs } from "./docs-share.js";
|
|
36
37
|
import { readNoteFile } from "./notes-file.js";
|
|
37
38
|
import { cacheAgeLabel, cacheCoverage, readThroughCache } from "./read-cache.js";
|
|
38
39
|
const TAG = "[docs cli]";
|
|
@@ -62,6 +63,10 @@ export async function runDocs(command, io) {
|
|
|
62
63
|
return exportDoc(command, door);
|
|
63
64
|
case "mutation-status":
|
|
64
65
|
return mutationStatusDocs(command, door);
|
|
66
|
+
case "share":
|
|
67
|
+
return shareDocs(command, door);
|
|
68
|
+
case "access":
|
|
69
|
+
return accessDocs(command, door);
|
|
65
70
|
}
|
|
66
71
|
}
|
|
67
72
|
/** `--parent`, `--query`, `--word`, `--limit`, `--bodies` (BLI-10450) as the door's own query parameters (BLI-3737). */
|
|
@@ -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
|
+
}
|
package/dist/commands/issue.js
CHANGED
|
@@ -31,6 +31,7 @@ import { assigneeLabel, beginAssigneeDirectoryFetch, finishAssigneeDirectory, wi
|
|
|
31
31
|
import { cacheAgeLabel, cacheCoverage, readThroughCache } from "./read-cache.js";
|
|
32
32
|
import { readTicketBoard } from "./tickets-board.js";
|
|
33
33
|
import { showIssue } from "./issue-show.js";
|
|
34
|
+
import { addIssuePageQuery, pageFieldsOf, pageFooter, streamAllIssues } from "./issue-list-page.js";
|
|
34
35
|
import { READ_DEADLINE_MS, TAG, resolveProjectId, sizeForEstimate, withSizeLabel, } from "./issue-contracts.js";
|
|
35
36
|
import { commentIssue, createIssue, moveIssue, updateIssue } from "./issue-write.js";
|
|
36
37
|
import { listIssueVersions, restoreIssueVersion } from "./issue-versions.js";
|
|
@@ -85,7 +86,7 @@ export function listGapLines(body, requestedLimit) {
|
|
|
85
86
|
const returned = body.issues?.length ?? 0;
|
|
86
87
|
const completeness = body.completeness;
|
|
87
88
|
if (completeness?.status === "partial") {
|
|
88
|
-
lines.push(`${TAG} partial list: ${completeness.returned} of ${completeness.total} matching issues; narrow the filter
|
|
89
|
+
lines.push(`${TAG} partial list: ${completeness.returned} of ${completeness.total} matching issues; narrow the filter, continue with --cursor, or use --all.`);
|
|
89
90
|
}
|
|
90
91
|
else if (completeness?.status === "unknown") {
|
|
91
92
|
lines.push(`${TAG} list may be partial: ${completeness.returned} issues filled the limit and Tower could not count the rest.`);
|
|
@@ -110,6 +111,10 @@ async function listIssues(command, door) {
|
|
|
110
111
|
const built = await buildIssueListQuery(command, door);
|
|
111
112
|
if (!built.ok)
|
|
112
113
|
return built.exitCode;
|
|
114
|
+
addIssuePageQuery(built.query, command);
|
|
115
|
+
// BLI-21315: a streamed export never goes through the read cache or buffers.
|
|
116
|
+
if (command.all)
|
|
117
|
+
return streamAllIssues(command, door, built.query);
|
|
113
118
|
const suffix = built.query.toString();
|
|
114
119
|
// BLI-10579: the assignee directory (`/api/team/members`) is the whole org
|
|
115
120
|
// roster regardless of which issues come back — it never actually depended
|
|
@@ -131,7 +136,7 @@ async function listIssues(command, door) {
|
|
|
131
136
|
const body = answer.body;
|
|
132
137
|
const issues = body.issues ?? [];
|
|
133
138
|
// BLI-10447: the gaps go to stderr in both modes, so `--json` stdout stays one object.
|
|
134
|
-
for (const line of listGapLines(body, command.limit ??
|
|
139
|
+
for (const line of listGapLines(body, command.limit ?? 100))
|
|
135
140
|
writeLine(door.io.stderr, line);
|
|
136
141
|
// BLI-10450: one extra call for the whole list, never one per row — the
|
|
137
142
|
// uuid `assignee_id` alone forced a second `cockpit team members` call and
|
|
@@ -144,6 +149,7 @@ async function listIssues(command, door) {
|
|
|
144
149
|
...cacheCoverage(answer),
|
|
145
150
|
issues: issues.map((issue) => withSizeLabel(withAssigneeIdentity(issue, directory))),
|
|
146
151
|
...(body.completeness ? { completeness: body.completeness } : {}),
|
|
152
|
+
...pageFieldsOf(body),
|
|
147
153
|
...(body.outside_project ? { outside_project: body.outside_project } : {}),
|
|
148
154
|
});
|
|
149
155
|
}
|
|
@@ -157,6 +163,9 @@ async function listIssues(command, door) {
|
|
|
157
163
|
writeLine(door.io.stdout, issueLine(issue, directory));
|
|
158
164
|
writeLine(door.io.stdout, "");
|
|
159
165
|
writeLine(door.io.stdout, `${issues.length} issue(s).`);
|
|
166
|
+
const footer = pageFooter(body, "issues");
|
|
167
|
+
if (footer)
|
|
168
|
+
writeLine(door.io.stdout, footer);
|
|
160
169
|
return 0;
|
|
161
170
|
}
|
|
162
171
|
/** The list query for the filters given, or the exit code of the project lookup that failed. */
|
|
@@ -230,7 +239,12 @@ function historyChange(row) {
|
|
|
230
239
|
}
|
|
231
240
|
async function issueHistory(command, door) {
|
|
232
241
|
const ref = command.issueRef ?? "";
|
|
233
|
-
const
|
|
242
|
+
const params = new URLSearchParams();
|
|
243
|
+
if (command.limit !== undefined)
|
|
244
|
+
params.set("limit", String(command.limit));
|
|
245
|
+
if (command.cursor)
|
|
246
|
+
params.set("cursor", command.cursor);
|
|
247
|
+
const query = params.toString() ? `?${params.toString()}` : "";
|
|
234
248
|
const answer = await askAgentDoor(door, {
|
|
235
249
|
path: `/api/work/issues/${encodeURIComponent(ref)}/history${query}`,
|
|
236
250
|
method: "GET",
|
|
@@ -239,9 +253,10 @@ async function issueHistory(command, door) {
|
|
|
239
253
|
});
|
|
240
254
|
if (!answer.ok)
|
|
241
255
|
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
242
|
-
const
|
|
256
|
+
const historyBody = answer.body;
|
|
257
|
+
const history = historyBody.history ?? [];
|
|
243
258
|
if (door.json)
|
|
244
|
-
return emitAgentDoor(door, { ok: true, history });
|
|
259
|
+
return emitAgentDoor(door, { ok: true, history, ...pageFieldsOf(historyBody) });
|
|
245
260
|
if (history.length === 0) {
|
|
246
261
|
writeLine(door.io.stdout, `No recorded changes for ${ref}.`);
|
|
247
262
|
return 0;
|
|
@@ -251,5 +266,8 @@ async function issueHistory(command, door) {
|
|
|
251
266
|
}
|
|
252
267
|
writeLine(door.io.stdout, "");
|
|
253
268
|
writeLine(door.io.stdout, `${history.length} change(s).`);
|
|
269
|
+
const footer = pageFooter(historyBody, "changes");
|
|
270
|
+
if (footer)
|
|
271
|
+
writeLine(door.io.stdout, footer);
|
|
254
272
|
return 0;
|
|
255
273
|
}
|