@bli-cockpit/cli 0.2.241 → 0.2.243
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/git-state.js +1 -0
- package/dist/adapters/raw-evidence-collection-state.js +22 -11
- 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-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 +279 -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-args-tower.js +1 -0
- package/dist/commands/local-args.js +2 -0
- package/dist/commands/local-auth.js +1 -1
- package/dist/commands/local-help-commands-tower-ops.js +2 -0
- package/dist/commands/local-help-commands-tower.js +48 -32
- package/dist/commands/local-help-commands.js +1 -0
- 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 +4 -2
- package/dist/commands/local.js +2 -0
- 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-draft.js +193 -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 +77 -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/push.js +50 -0
- 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-upload.js +2 -0
- package/dist/commands/session-sync.js +1 -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/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,162 @@
|
|
|
1
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
2
|
+
import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
|
|
3
|
+
import { BODY_MAX_CHARS, READ_DEADLINE_MS, WRITE_DEADLINE_MS, resolveRefs, sizeForEstimate } from "./issue-contracts.js";
|
|
4
|
+
const TAG = "[ticket templates cli]";
|
|
5
|
+
const presetText = (t) => {
|
|
6
|
+
const parts = [];
|
|
7
|
+
if (t.state)
|
|
8
|
+
parts.push(`state ${t.state}`);
|
|
9
|
+
if (t.priority !== null)
|
|
10
|
+
parts.push(`priority ${t.priority}`);
|
|
11
|
+
if (t.estimate !== null)
|
|
12
|
+
parts.push(`size ${sizeForEstimate(t.estimate) ?? t.estimate}`);
|
|
13
|
+
if (t.project_id)
|
|
14
|
+
parts.push(`project ${t.project_id}`);
|
|
15
|
+
if (t.label_names.length > 0)
|
|
16
|
+
parts.push(`labels ${t.label_names.join(", ")}`);
|
|
17
|
+
return parts.length === 0 ? "no presets" : parts.join(", ");
|
|
18
|
+
};
|
|
19
|
+
const templateLine = (t) => `${t.id} ${t.name} [v${t.version}${t.archived_at ? ", archived" : ""}${t.mine ? ", yours" : ""}] ${presetText(t)}`;
|
|
20
|
+
/** `--json` rows get a `size` letter beside the raw estimate, like every ticket read. */
|
|
21
|
+
const withSize = (row) => ({ ...row, size: sizeForEstimate(row.estimate ?? null) });
|
|
22
|
+
export async function runTemplates(command, io, door) {
|
|
23
|
+
const t = command.templates;
|
|
24
|
+
const ref = t.ref ? encodeURIComponent(t.ref) : "";
|
|
25
|
+
switch (t.verb) {
|
|
26
|
+
case "list": {
|
|
27
|
+
const answer = await askAgentDoor(door, {
|
|
28
|
+
path: `/api/work/templates${t.includeArchived ? "?include_archived=true" : ""}`,
|
|
29
|
+
method: "GET", label: "ticket templates list", timeoutMs: READ_DEADLINE_MS,
|
|
30
|
+
});
|
|
31
|
+
if (!answer.ok)
|
|
32
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
33
|
+
const body = answer.body;
|
|
34
|
+
const templates = body.templates ?? [];
|
|
35
|
+
if (door.json)
|
|
36
|
+
return emitAgentDoor(door, { ok: true, ...body, templates: templates.map(withSize) });
|
|
37
|
+
if (templates.length === 0)
|
|
38
|
+
writeLine(door.io.stdout, "No templates yet. Save one with `cockpit ticket templates save --name <name>`.");
|
|
39
|
+
for (const row of templates)
|
|
40
|
+
writeLine(door.io.stdout, templateLine(row));
|
|
41
|
+
if (body.archived_hidden)
|
|
42
|
+
writeLine(door.io.stdout, `\n${body.archived_hidden} archived template(s) not shown (--include-archived).`);
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
case "show": {
|
|
46
|
+
const answer = await askAgentDoor(door, { path: `/api/work/templates/${ref}`, method: "GET", label: "ticket templates show", timeoutMs: READ_DEADLINE_MS });
|
|
47
|
+
if (!answer.ok)
|
|
48
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
49
|
+
const body = answer.body;
|
|
50
|
+
if (door.json)
|
|
51
|
+
return emitAgentDoor(door, { ok: true, template: withSize(body.template) });
|
|
52
|
+
const row = body.template;
|
|
53
|
+
writeLine(door.io.stdout, templateLine(row));
|
|
54
|
+
writeLine(door.io.stdout, row.description ? `\n${row.description}` : "\n(no description)");
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
case "versions": {
|
|
58
|
+
const answer = await askAgentDoor(door, { path: `/api/work/templates/${ref}/versions`, method: "GET", label: "ticket templates versions", timeoutMs: READ_DEADLINE_MS });
|
|
59
|
+
if (!answer.ok)
|
|
60
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
61
|
+
const body = answer.body;
|
|
62
|
+
if (door.json)
|
|
63
|
+
return emitAgentDoor(door, { ok: true, ...body, template: withSize(body.template), versions: (body.versions ?? []).map((v) => withSize(v)) });
|
|
64
|
+
writeLine(door.io.stdout, `${body.template.name}: ${body.versions?.length ?? 0} version(s), current v${body.template.version}.`);
|
|
65
|
+
for (const v of body.versions ?? [])
|
|
66
|
+
writeLine(door.io.stdout, `v${v.version} ${v.created_at} ${v.name}${v.archived ? " (archived)" : ""}`);
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
case "save":
|
|
70
|
+
case "update":
|
|
71
|
+
return writeTemplate(command, io, door, ref);
|
|
72
|
+
case "apply":
|
|
73
|
+
return applyTemplate(command, io, door, ref);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** `--description -` reads the text from stdin; anything else is the text itself. */
|
|
77
|
+
async function readDescription(value, io) {
|
|
78
|
+
if (value !== "-")
|
|
79
|
+
return { ok: true, text: value };
|
|
80
|
+
if (isInteractiveStdin(io))
|
|
81
|
+
return { ok: false, detail: "--description - reads the text from stdin, and nothing is piped in." };
|
|
82
|
+
try {
|
|
83
|
+
return { ok: true, text: await readPipedText(io.stdin, { maxChars: BODY_MAX_CHARS, overflowMessage: `A template description is limited to ${BODY_MAX_CHARS} characters.` }) };
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
return { ok: false, detail: error instanceof Error ? error.message : String(error) };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function writeTemplate(command, io, door, ref) {
|
|
90
|
+
const t = command.templates;
|
|
91
|
+
const description = await readDescription(t.description, io);
|
|
92
|
+
if (!description.ok)
|
|
93
|
+
return failAgentDoor(door, TAG, "invalid_body", description.detail);
|
|
94
|
+
const payload = {
|
|
95
|
+
...(t.verb === "save" && command.requestId ? { request_id: command.requestId } : {}),
|
|
96
|
+
...(t.verb === "update" && t.expectedUpdatedAt ? { expected_updated_at: t.expectedUpdatedAt } : {}),
|
|
97
|
+
...(t.name === undefined ? {} : { name: t.name }),
|
|
98
|
+
...(description.text === undefined ? {} : { description: description.text }),
|
|
99
|
+
...(t.state === undefined ? {} : { state: t.state }),
|
|
100
|
+
...(t.priority === undefined ? {} : { priority: t.priority }),
|
|
101
|
+
...(t.estimate === undefined ? {} : { estimate: t.estimate }),
|
|
102
|
+
...(t.project === undefined ? {} : { project: t.project }),
|
|
103
|
+
...(t.labels ? { label_names: t.labels } : {}),
|
|
104
|
+
...(t.archived === undefined ? {} : { archived: t.archived }),
|
|
105
|
+
};
|
|
106
|
+
const answer = await askAgentDoor(door, {
|
|
107
|
+
path: t.verb === "save" ? "/api/work/templates" : `/api/work/templates/${ref}`,
|
|
108
|
+
method: t.verb === "save" ? "POST" : "PATCH",
|
|
109
|
+
label: `ticket templates ${t.verb}`,
|
|
110
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
111
|
+
body: payload,
|
|
112
|
+
});
|
|
113
|
+
if (!answer.ok) {
|
|
114
|
+
const mutation = answer.body?.mutation;
|
|
115
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail, mutation ? { mutation } : {});
|
|
116
|
+
}
|
|
117
|
+
const body = answer.body;
|
|
118
|
+
console.error(`${TAG} ${t.verb} done`, JSON.stringify({ template_id: body.template.id, version: body.template.version, replayed: Boolean(body.replayed) }));
|
|
119
|
+
if (door.json)
|
|
120
|
+
return emitAgentDoor(door, { ok: true, ...body, template: withSize(body.template) });
|
|
121
|
+
writeLine(door.io.stdout, `${t.verb === "save" ? (body.replayed ? "Already saved" : "Saved") : "Updated"}: ${body.template.name} (${body.template.id}), version ${body.template.version}${body.template.archived_at ? ", archived" : ""}.`);
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
async function applyTemplate(command, io, door, ref) {
|
|
125
|
+
const t = command.templates;
|
|
126
|
+
const description = await readDescription(t.description, io);
|
|
127
|
+
if (!description.ok)
|
|
128
|
+
return failAgentDoor(door, TAG, "invalid_body", description.detail);
|
|
129
|
+
// A project name or a parent identifier is resolved to ids here, the way `ticket create` does.
|
|
130
|
+
const refs = await resolveRefs({ project: t.project ?? undefined, parentRef: t.parentRef }, door);
|
|
131
|
+
if (!refs.ok)
|
|
132
|
+
return failAgentDoor(door, TAG, refs.reason, refs.detail);
|
|
133
|
+
const payload = {
|
|
134
|
+
...(command.requestId ? { request_id: command.requestId } : {}),
|
|
135
|
+
...(t.title ? { title: t.title } : {}),
|
|
136
|
+
...(description.text === undefined ? {} : { description: description.text }),
|
|
137
|
+
...(t.state === undefined ? {} : { state: t.state }),
|
|
138
|
+
...(t.priority === undefined ? {} : { priority: t.priority }),
|
|
139
|
+
...(t.estimate === undefined ? {} : { estimate: t.estimate }),
|
|
140
|
+
...(t.project === null ? { project_id: null } : refs.projectId ? { project_id: refs.projectId } : {}),
|
|
141
|
+
...(t.labels ? { labels: t.labels } : {}),
|
|
142
|
+
...(t.assignee ? { assignee_id: t.assignee } : {}),
|
|
143
|
+
...(refs.parentId ? { parent_id: refs.parentId } : {}),
|
|
144
|
+
...(t.due === undefined ? {} : { due_date: t.due }),
|
|
145
|
+
};
|
|
146
|
+
const answer = await askAgentDoor(door, {
|
|
147
|
+
path: `/api/work/templates/${ref}/apply`, method: "POST", label: "ticket templates apply", timeoutMs: WRITE_DEADLINE_MS, body: payload,
|
|
148
|
+
});
|
|
149
|
+
if (!answer.ok) {
|
|
150
|
+
const mutation = answer.body?.mutation;
|
|
151
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail, mutation ? { mutation } : {});
|
|
152
|
+
}
|
|
153
|
+
const body = answer.body;
|
|
154
|
+
console.error(`${TAG} apply done`, JSON.stringify({ template_id: body.template?.id, version: body.template?.version, labels: body.labels?.status }));
|
|
155
|
+
if (door.json)
|
|
156
|
+
return emitAgentDoor(door, { ok: true, ...body, issue: withSize(body.issue) });
|
|
157
|
+
writeLine(door.io.stdout, `Created ${body.issue.identifier ?? ""} from template v${body.template?.version ?? "?"}: ${body.issue.title ?? ""}`);
|
|
158
|
+
if (body.labels?.status === "failed") {
|
|
159
|
+
writeLine(door.io.stderr, `The ticket exists but its labels (${(body.labels.names ?? []).join(", ")}) were not applied: ${body.labels.reason}. Add them with \`cockpit ticket label\`.`);
|
|
160
|
+
}
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit ticket views [list|show|save|update|favorite|unfavorite|settings]` argument parsing
|
|
3
|
+
* (BLI-21686, P8). `views` is an `issue` action; its flags are read here so `local-args-tower-work.ts`
|
|
4
|
+
* only lists their names, the way `issue-relations.ts` does for relations.
|
|
5
|
+
*/
|
|
6
|
+
import { optionalNonEmpty, optionalUrl } from "./local-arg-values.js";
|
|
7
|
+
export const VIEW_VALUE_FLAGS = ["--name", "--layout", "--hide", "--show", "--expected-updated-at", "--description"];
|
|
8
|
+
export const VIEW_BOOLEAN_FLAGS = ["--shared", "--private", "--archive", "--restore", "--include-archived"];
|
|
9
|
+
export const VIEW_FIELDS = ["priority", "labels", "project", "assignee", "updated"];
|
|
10
|
+
const VIEW_VERBS = ["list", "show", "save", "update", "favorite", "unfavorite", "settings"];
|
|
11
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
12
|
+
const STATES = ["backlog", "todo", "in_progress", "in_review", "done", "canceled"];
|
|
13
|
+
function listOf(values, flag) {
|
|
14
|
+
return (values.flagValues.get(flag) ?? []).map((v) => v.trim()).filter(Boolean);
|
|
15
|
+
}
|
|
16
|
+
export function parseViewArgs(values, rest) {
|
|
17
|
+
const first = rest[0];
|
|
18
|
+
const verb = (first === undefined ? "list" : first);
|
|
19
|
+
if (!VIEW_VERBS.includes(verb)) {
|
|
20
|
+
throw new Error(`Unknown views command: ${first}. Try list, show, save, update, favorite, unfavorite or settings.`);
|
|
21
|
+
}
|
|
22
|
+
const positional = rest.slice(first === undefined ? 0 : 1);
|
|
23
|
+
const needsRef = verb === "show" || verb === "update" || verb === "favorite" || verb === "unfavorite";
|
|
24
|
+
if (needsRef && positional.length !== 1)
|
|
25
|
+
throw new Error(`ticket views ${verb} takes exactly one view id or name.`);
|
|
26
|
+
if (!needsRef && positional.length > 0)
|
|
27
|
+
throw new Error(`ticket views ${verb} does not take "${positional[0]}".`);
|
|
28
|
+
const given = (flag) => values.flags.has(flag) || values.booleans.has(flag);
|
|
29
|
+
const definitionFlags = ["--name", "--description", "--state", "--assignee", "--project", "--label", "--q", "--layout", "--sort", "--dir", "--shared", "--private"];
|
|
30
|
+
const writes = verb === "save" || verb === "update";
|
|
31
|
+
for (const flag of definitionFlags) {
|
|
32
|
+
if (given(flag) && !writes)
|
|
33
|
+
throw new Error(`${flag} belongs to \`cockpit ticket views save\` and \`update\`.`);
|
|
34
|
+
}
|
|
35
|
+
if (!writes && (given("--archive") || given("--restore") || given("--expected-updated-at"))) {
|
|
36
|
+
throw new Error("--archive, --restore and --expected-updated-at belong to `cockpit ticket views update`.");
|
|
37
|
+
}
|
|
38
|
+
if (verb === "save" && (given("--archive") || given("--restore") || given("--expected-updated-at"))) {
|
|
39
|
+
throw new Error("save takes --shared (or nothing for private); --archive, --restore and --expected-updated-at belong to `update`.");
|
|
40
|
+
}
|
|
41
|
+
if (given("--include-archived"))
|
|
42
|
+
throw new Error("--include-archived belongs to `cockpit ticket templates list`.");
|
|
43
|
+
if (verb !== "save" && given("--request-id"))
|
|
44
|
+
throw new Error("--request-id belongs to `cockpit ticket views save`.");
|
|
45
|
+
if (given("--private") && given("--shared"))
|
|
46
|
+
throw new Error("Give --shared or --private, not both.");
|
|
47
|
+
if (given("--archive") && given("--restore"))
|
|
48
|
+
throw new Error("Give --archive or --restore, not both.");
|
|
49
|
+
if (verb !== "settings" && (given("--hide") || given("--show")))
|
|
50
|
+
throw new Error("--hide and --show belong to `cockpit ticket views settings`.");
|
|
51
|
+
if (verb === "list" || verb === "show" || verb === "favorite" || verb === "unfavorite" || verb === "settings") {
|
|
52
|
+
if (given("--expected-revision") || given("--limit"))
|
|
53
|
+
throw new Error("That flag does not belong to ticket views.");
|
|
54
|
+
}
|
|
55
|
+
const name = optionalNonEmpty(values.flags.get("--name"));
|
|
56
|
+
if (verb === "save" && !name)
|
|
57
|
+
throw new Error('ticket views save needs --name "<name>".');
|
|
58
|
+
const filters = {};
|
|
59
|
+
const state = optionalNonEmpty(values.flags.get("--state"));
|
|
60
|
+
if (state) {
|
|
61
|
+
if (!STATES.includes(state))
|
|
62
|
+
throw new Error(`--state must be one of: ${STATES.join(", ")}.`);
|
|
63
|
+
filters["state"] = state;
|
|
64
|
+
}
|
|
65
|
+
const assignee = optionalNonEmpty(values.flags.get("--assignee"));
|
|
66
|
+
if (assignee) {
|
|
67
|
+
if (assignee !== "me" && assignee !== "unassigned" && !UUID.test(assignee)) {
|
|
68
|
+
throw new Error('--assignee must be "me", "unassigned" or a person\'s uuid. "me" stays "me" and means whoever opens the view.');
|
|
69
|
+
}
|
|
70
|
+
filters["assignee"] = assignee;
|
|
71
|
+
}
|
|
72
|
+
for (const [flag, key] of [["--project", "project"], ["--label", "label"], ["--q", "q"]]) {
|
|
73
|
+
const value = optionalNonEmpty(values.flags.get(flag));
|
|
74
|
+
if (value)
|
|
75
|
+
filters[key] = value;
|
|
76
|
+
}
|
|
77
|
+
const layout = optionalNonEmpty(values.flags.get("--layout"));
|
|
78
|
+
if (layout && layout !== "list" && layout !== "board")
|
|
79
|
+
throw new Error("--layout must be list or board.");
|
|
80
|
+
const sort = optionalNonEmpty(values.flags.get("--sort"));
|
|
81
|
+
if (sort && sort !== "updated_at" && sort !== "created_at")
|
|
82
|
+
throw new Error("--sort must be updated_at or created_at.");
|
|
83
|
+
const dir = optionalNonEmpty(values.flags.get("--dir"));
|
|
84
|
+
if (dir && dir !== "asc" && dir !== "desc")
|
|
85
|
+
throw new Error("--dir must be asc or desc.");
|
|
86
|
+
const expectedUpdatedAt = optionalNonEmpty(values.flags.get("--expected-updated-at"));
|
|
87
|
+
if (expectedUpdatedAt && Number.isNaN(Date.parse(expectedUpdatedAt))) {
|
|
88
|
+
throw new Error("--expected-updated-at must be the updated_at timestamp you last read.");
|
|
89
|
+
}
|
|
90
|
+
const requestId = optionalNonEmpty(values.flags.get("--request-id"));
|
|
91
|
+
if (requestId && !UUID.test(requestId))
|
|
92
|
+
throw new Error("--request-id must be a UUID.");
|
|
93
|
+
const rawDescription = values.flags.get("--description");
|
|
94
|
+
const hide = listOf(values, "--hide");
|
|
95
|
+
const show = listOf(values, "--show");
|
|
96
|
+
for (const field of [...hide, ...show]) {
|
|
97
|
+
if (!VIEW_FIELDS.includes(field))
|
|
98
|
+
throw new Error(`"${field}" is not a field. Use ${VIEW_FIELDS.join(", ")}.`);
|
|
99
|
+
}
|
|
100
|
+
if (verb === "settings" && (hide.length === 0 && show.length === 0) === false && hide.some((f) => show.includes(f))) {
|
|
101
|
+
throw new Error("A field cannot be in both --hide and --show.");
|
|
102
|
+
}
|
|
103
|
+
if (verb === "update" && !name && rawDescription === undefined && Object.keys(filters).length === 0 && !layout && !sort && !dir
|
|
104
|
+
&& !given("--shared") && !given("--private") && !given("--archive") && !given("--restore")) {
|
|
105
|
+
throw new Error("ticket views update needs a field to change: --name, a filter, --layout, --sort, --dir, --shared, --private, --archive or --restore.");
|
|
106
|
+
}
|
|
107
|
+
const views = {
|
|
108
|
+
verb,
|
|
109
|
+
...(needsRef ? { ref: positional[0] } : {}),
|
|
110
|
+
...(name ? { name } : {}),
|
|
111
|
+
...(rawDescription === undefined ? {} : { description: rawDescription.trim().toLowerCase() === "none" ? null : rawDescription }),
|
|
112
|
+
...(Object.keys(filters).length > 0 ? { filters } : {}),
|
|
113
|
+
...(layout ? { layout } : {}),
|
|
114
|
+
...(sort ? { sort } : {}),
|
|
115
|
+
...(dir ? { dir } : {}),
|
|
116
|
+
...(given("--shared") ? { visibility: "shared" } : given("--private") ? { visibility: "private" } : {}),
|
|
117
|
+
...(given("--archive") ? { archived: true } : given("--restore") ? { archived: false } : {}),
|
|
118
|
+
...(expectedUpdatedAt ? { expectedUpdatedAt } : {}),
|
|
119
|
+
...(hide.length > 0 ? { hide } : {}),
|
|
120
|
+
...(show.length > 0 ? { show } : {}),
|
|
121
|
+
};
|
|
122
|
+
return {
|
|
123
|
+
kind: "issue",
|
|
124
|
+
action: "views",
|
|
125
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
126
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
127
|
+
...(requestId ? { requestId } : {}),
|
|
128
|
+
json: values.booleans.has("--json"),
|
|
129
|
+
views,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
2
|
+
import { writeLine } from "./cli-io.js";
|
|
3
|
+
import { pageFieldsOf, pageFooter } from "./issue-list-page.js";
|
|
4
|
+
import { READ_DEADLINE_MS, WRITE_DEADLINE_MS } from "./issue-contracts.js";
|
|
5
|
+
const TAG = "[ticket views cli]";
|
|
6
|
+
const filterText = (filters) => {
|
|
7
|
+
const parts = Object.entries(filters).map(([key, value]) => `${key}=${value}`);
|
|
8
|
+
return parts.length === 0 ? "no filters" : parts.join(" ");
|
|
9
|
+
};
|
|
10
|
+
const viewLine = (view) => `${view.id} ${view.name} [${view.preset ? "built in" : view.visibility}${view.archived_at ? ", archived" : ""}${view.favorite ? ", favorite" : ""}] ${filterText(view.filters)}`;
|
|
11
|
+
export async function runViews(command, io, door) {
|
|
12
|
+
void io;
|
|
13
|
+
const views = command.views;
|
|
14
|
+
const ref = views.ref ? encodeURIComponent(views.ref) : "";
|
|
15
|
+
switch (views.verb) {
|
|
16
|
+
case "list":
|
|
17
|
+
return listViews(door, command);
|
|
18
|
+
case "show":
|
|
19
|
+
return showView(door, ref, views.ref ?? "");
|
|
20
|
+
case "favorite":
|
|
21
|
+
case "unfavorite": {
|
|
22
|
+
const answer = await askAgentDoor(door, {
|
|
23
|
+
path: `/api/work/views/${ref}/favorite`,
|
|
24
|
+
method: views.verb === "favorite" ? "POST" : "DELETE",
|
|
25
|
+
label: `ticket views ${views.verb}`,
|
|
26
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
27
|
+
});
|
|
28
|
+
if (!answer.ok)
|
|
29
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
30
|
+
const body = answer.body;
|
|
31
|
+
console.error(`${TAG} ${views.verb} done`, JSON.stringify({ view_id: body.view?.id ?? views.ref, count: body.favorite_view_ids?.length ?? 0 }));
|
|
32
|
+
if (door.json)
|
|
33
|
+
return emitAgentDoor(door, { ok: true, ...body });
|
|
34
|
+
writeLine(door.io.stdout, `${views.verb === "favorite" ? "Favorited" : "Removed from favorites"}: ${body.view?.name ?? views.ref}. ${body.favorite_view_ids?.length ?? 0} favorite(s).`);
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
case "settings":
|
|
38
|
+
return settings(door, command);
|
|
39
|
+
case "save":
|
|
40
|
+
case "update":
|
|
41
|
+
return writeView(door, command, ref);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function listViews(door, command) {
|
|
45
|
+
void command;
|
|
46
|
+
const answer = await askAgentDoor(door, { path: "/api/work/views", method: "GET", label: "ticket views list", timeoutMs: READ_DEADLINE_MS });
|
|
47
|
+
if (!answer.ok)
|
|
48
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
49
|
+
const body = answer.body;
|
|
50
|
+
if (door.json)
|
|
51
|
+
return emitAgentDoor(door, { ok: true, ...body });
|
|
52
|
+
const out = door.io.stdout;
|
|
53
|
+
if (body.my_work)
|
|
54
|
+
writeLine(out, viewLine(body.my_work));
|
|
55
|
+
for (const [title, rows] of [["Favorites", body.favorites], ["Mine", body.mine], ["Shared", body.shared]]) {
|
|
56
|
+
if (!rows || rows.length === 0)
|
|
57
|
+
continue;
|
|
58
|
+
writeLine(out, "");
|
|
59
|
+
writeLine(out, `${title}:`);
|
|
60
|
+
for (const view of rows)
|
|
61
|
+
writeLine(out, viewLine(view));
|
|
62
|
+
}
|
|
63
|
+
if (body.favorites_hidden)
|
|
64
|
+
writeLine(out, `\n${body.favorites_hidden} favorite(s) are not shown: archived, private to someone else or gone.`);
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
async function showView(door, ref, label) {
|
|
68
|
+
const answer = await askAgentDoor(door, { path: `/api/work/views/${ref}`, method: "GET", label: "ticket views show", timeoutMs: READ_DEADLINE_MS });
|
|
69
|
+
if (!answer.ok)
|
|
70
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
71
|
+
const body = answer.body;
|
|
72
|
+
if (door.json)
|
|
73
|
+
return emitAgentDoor(door, { ok: true, ...body, ...pageFieldsOf(body) });
|
|
74
|
+
const out = door.io.stdout;
|
|
75
|
+
const view = body.view;
|
|
76
|
+
writeLine(out, `${view?.name ?? label} (${view?.preset ? "built in" : view?.visibility}, ${view?.layout}, ${view?.sort} ${view?.dir})`);
|
|
77
|
+
writeLine(out, `Filters: ${filterText(view?.filters ?? {})}${view?.filters?.["assignee"] === "me" ? " (me is you)" : ""}`);
|
|
78
|
+
for (const issue of body.issues ?? [])
|
|
79
|
+
writeLine(out, `${(issue.identifier ?? "").padEnd(9)} ${(issue.state ?? "").padEnd(11)} ${issue.title ?? ""}`);
|
|
80
|
+
const footer = pageFooter(body, "tickets");
|
|
81
|
+
if (footer)
|
|
82
|
+
writeLine(out, footer);
|
|
83
|
+
if (body.href)
|
|
84
|
+
writeLine(out, `Open on the web: ${body.href}`);
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
async function settings(door, command) {
|
|
88
|
+
const { hide, show } = command.views;
|
|
89
|
+
const changing = Boolean(hide?.length || show?.length);
|
|
90
|
+
const answer = await askAgentDoor(door, {
|
|
91
|
+
path: "/api/work/view-settings",
|
|
92
|
+
method: changing ? "PATCH" : "GET",
|
|
93
|
+
label: "ticket views settings",
|
|
94
|
+
timeoutMs: changing ? WRITE_DEADLINE_MS : READ_DEADLINE_MS,
|
|
95
|
+
...(changing ? { body: { ...(hide?.length ? { hide } : {}), ...(show?.length ? { show } : {}) } } : {}),
|
|
96
|
+
});
|
|
97
|
+
if (!answer.ok)
|
|
98
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
99
|
+
const body = answer.body;
|
|
100
|
+
if (door.json)
|
|
101
|
+
return emitAgentDoor(door, { ok: true, ...body });
|
|
102
|
+
writeLine(door.io.stdout, `Hidden fields on your ticket rows: ${body.hidden_fields?.length ? body.hidden_fields.join(", ") : "none"}. Fields: ${(body.available_fields ?? []).join(", ")}.`);
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
async function writeView(door, command, ref) {
|
|
106
|
+
const v = command.views;
|
|
107
|
+
const payload = {
|
|
108
|
+
...(v.verb === "save" && command.requestId ? { request_id: command.requestId } : {}),
|
|
109
|
+
...(v.verb === "update" && v.expectedUpdatedAt ? { expected_updated_at: v.expectedUpdatedAt } : {}),
|
|
110
|
+
...(v.name === undefined ? {} : { name: v.name }),
|
|
111
|
+
...(v.description === undefined ? {} : { description: v.description }),
|
|
112
|
+
...(v.filters ? { filters: v.filters } : {}),
|
|
113
|
+
...(v.layout ? { layout: v.layout } : {}),
|
|
114
|
+
...(v.sort ? { sort: v.sort } : {}),
|
|
115
|
+
...(v.dir ? { dir: v.dir } : {}),
|
|
116
|
+
...(v.visibility ? { visibility: v.visibility } : {}),
|
|
117
|
+
...(v.archived === undefined ? {} : { archived: v.archived }),
|
|
118
|
+
};
|
|
119
|
+
const answer = await askAgentDoor(door, {
|
|
120
|
+
path: v.verb === "save" ? "/api/work/views" : `/api/work/views/${ref}`,
|
|
121
|
+
method: v.verb === "save" ? "POST" : "PATCH",
|
|
122
|
+
label: `ticket views ${v.verb}`,
|
|
123
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
124
|
+
body: payload,
|
|
125
|
+
});
|
|
126
|
+
if (!answer.ok) {
|
|
127
|
+
const mutation = answer.body?.mutation;
|
|
128
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail, mutation ? { mutation } : {});
|
|
129
|
+
}
|
|
130
|
+
const body = answer.body;
|
|
131
|
+
console.error(`${TAG} ${v.verb} done`, JSON.stringify({ view_id: body.view?.id, replayed: Boolean(body.replayed) }));
|
|
132
|
+
if (door.json)
|
|
133
|
+
return emitAgentDoor(door, { ok: true, ...body });
|
|
134
|
+
writeLine(door.io.stdout, `${v.verb === "save" ? (body.replayed ? "Already saved" : "Saved") : "Updated"}: ${body.view?.name ?? ""} (${body.view?.id ?? ""}), ${body.view?.visibility ?? ""}${body.view?.archived_at ? ", archived" : ""}.`);
|
|
135
|
+
if (body.view?.href)
|
|
136
|
+
writeLine(door.io.stdout, `Open on the web: ${body.view.href}`);
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit ticket notifications [set|receipts]` argument parsing (BLI-21736, P10). An `issue` action; its flags are
|
|
3
|
+
* read here so `local-args-tower-work.ts` only lists the names.
|
|
4
|
+
*/
|
|
5
|
+
import { optionalNonEmpty, optionalPositiveInteger, optionalUrl } from "./local-arg-values.js";
|
|
6
|
+
export const NOTIFY_ACTIONS = ["notifications"];
|
|
7
|
+
export const NOTIFY_VALUE_FLAGS = ["--slack", "--notification"];
|
|
8
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
9
|
+
export function isNotifyAction(action) {
|
|
10
|
+
return NOTIFY_ACTIONS.includes(action);
|
|
11
|
+
}
|
|
12
|
+
export function parseNotifyArgs(values, rest) {
|
|
13
|
+
const given = (flag) => values.flags.has(flag) || values.booleans.has(flag);
|
|
14
|
+
const word = rest[0];
|
|
15
|
+
if (word !== undefined && word !== "set" && word !== "receipts") {
|
|
16
|
+
throw new Error(`ticket notifications does not take "${word.slice(0, 40)}". Try: notifications, notifications set --slack on|off, notifications receipts.`);
|
|
17
|
+
}
|
|
18
|
+
if (rest.length > 1)
|
|
19
|
+
throw new Error(`ticket notifications ${word} takes no further arguments.`);
|
|
20
|
+
for (const flag of ["--request-id", "--expected-revision", "--all", "--kind", "--unread"]) {
|
|
21
|
+
if (given(flag))
|
|
22
|
+
throw new Error(`${flag} does not apply to \`cockpit ticket notifications\`.`);
|
|
23
|
+
}
|
|
24
|
+
const notify = { sub: word === "set" ? "set" : word === "receipts" ? "receipts" : "show" };
|
|
25
|
+
const onlyFor = (flags, sub) => {
|
|
26
|
+
for (const flag of flags)
|
|
27
|
+
if (given(flag))
|
|
28
|
+
throw new Error(`${flag} belongs to \`cockpit ticket notifications ${sub}\`.`);
|
|
29
|
+
};
|
|
30
|
+
if (notify.sub === "set") {
|
|
31
|
+
onlyFor(["--notification", "--before", "--limit"], "receipts");
|
|
32
|
+
const slack = optionalNonEmpty(values.flags.get("--slack"));
|
|
33
|
+
if (slack !== "on" && slack !== "off")
|
|
34
|
+
throw new Error("ticket notifications set needs --slack on or --slack off.");
|
|
35
|
+
notify.slackEnabled = slack === "on";
|
|
36
|
+
}
|
|
37
|
+
else if (notify.sub === "receipts") {
|
|
38
|
+
onlyFor(["--slack"], "set");
|
|
39
|
+
const notification = optionalNonEmpty(values.flags.get("--notification"));
|
|
40
|
+
if (notification) {
|
|
41
|
+
if (!UUID.test(notification))
|
|
42
|
+
throw new Error("--notification must be a notification id. Copy it from `cockpit ticket inbox --json`.");
|
|
43
|
+
notify.notification = notification;
|
|
44
|
+
}
|
|
45
|
+
const before = optionalNonEmpty(values.flags.get("--before"));
|
|
46
|
+
if (before)
|
|
47
|
+
notify.before = before;
|
|
48
|
+
const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
|
|
49
|
+
if (limit !== undefined) {
|
|
50
|
+
if (limit > 100)
|
|
51
|
+
throw new Error("--limit is at most 100.");
|
|
52
|
+
notify.limit = limit;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
onlyFor(["--slack"], "set");
|
|
57
|
+
onlyFor(["--notification", "--before", "--limit"], "receipts");
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
kind: "issue",
|
|
61
|
+
action: "notifications",
|
|
62
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
63
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
64
|
+
json: values.booleans.has("--json"),
|
|
65
|
+
notify,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
2
|
+
import { writeLine } from "./cli-io.js";
|
|
3
|
+
import { READ_DEADLINE_MS, WRITE_DEADLINE_MS } from "./issue-contracts.js";
|
|
4
|
+
const TAG = "[ticket notifications cli]";
|
|
5
|
+
function writeSettings(door, settings) {
|
|
6
|
+
const out = door.io.stdout;
|
|
7
|
+
writeLine(out, `Slack delivery: ${settings.slack_enabled ? "on" : "off"}${settings.slack_enabled_at && settings.slack_enabled ? ` (since ${settings.slack_enabled_at})` : ""}`);
|
|
8
|
+
if (settings.slack_linked === null)
|
|
9
|
+
writeLine(out, `Slack account: could not check (${settings.slack_linked_reason ?? "unknown"}).`);
|
|
10
|
+
else
|
|
11
|
+
writeLine(out, settings.slack_linked ? "Slack account: linked." : "Slack account: none linked, so delivery would fail with slack_identity_missing.");
|
|
12
|
+
}
|
|
13
|
+
export async function runWorkNotify(command, io, door) {
|
|
14
|
+
void io;
|
|
15
|
+
const notify = command.notify;
|
|
16
|
+
if (notify.sub === "receipts") {
|
|
17
|
+
const query = new URLSearchParams();
|
|
18
|
+
if (notify.notification)
|
|
19
|
+
query.set("notification_id", notify.notification);
|
|
20
|
+
if (notify.before)
|
|
21
|
+
query.set("before", notify.before);
|
|
22
|
+
if (notify.limit !== undefined)
|
|
23
|
+
query.set("limit", String(notify.limit));
|
|
24
|
+
const suffix = query.toString();
|
|
25
|
+
const answer = await askAgentDoor(door, {
|
|
26
|
+
path: `/api/work/notifications/receipts${suffix ? `?${suffix}` : ""}`,
|
|
27
|
+
method: "GET",
|
|
28
|
+
label: "ticket notifications receipts",
|
|
29
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
30
|
+
});
|
|
31
|
+
if (!answer.ok)
|
|
32
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
33
|
+
const body = answer.body;
|
|
34
|
+
if (door.json)
|
|
35
|
+
return emitAgentDoor(door, { ok: true, ...body });
|
|
36
|
+
const receipts = body.receipts ?? [];
|
|
37
|
+
if (receipts.length === 0)
|
|
38
|
+
writeLine(door.io.stdout, "No delivery receipts yet.");
|
|
39
|
+
for (const receipt of receipts) {
|
|
40
|
+
writeLine(door.io.stdout, `${receipt.id} ${receipt.outcome.padEnd(10)} ${(receipt.reason ?? "-").padEnd(22)} ${(receipt.identifier ?? "(ticket not readable)").padEnd(9)} ${receipt.created_at}`);
|
|
41
|
+
}
|
|
42
|
+
if (body.has_more && body.next_cursor)
|
|
43
|
+
writeLine(door.io.stdout, `More receipts available. Continue with --before ${body.next_cursor}`);
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
const setting = notify.sub === "set";
|
|
47
|
+
const answer = await askAgentDoor(door, {
|
|
48
|
+
path: "/api/work/notifications/settings",
|
|
49
|
+
method: setting ? "PUT" : "GET",
|
|
50
|
+
label: setting ? "ticket notifications set" : "ticket notifications",
|
|
51
|
+
timeoutMs: setting ? WRITE_DEADLINE_MS : READ_DEADLINE_MS,
|
|
52
|
+
...(setting ? { body: { slack_enabled: notify.slackEnabled } } : {}),
|
|
53
|
+
});
|
|
54
|
+
if (!answer.ok)
|
|
55
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
56
|
+
const body = answer.body;
|
|
57
|
+
console.error(`${TAG} ${setting ? "set" : "read"} done`, JSON.stringify({ slack_enabled: body.settings?.slack_enabled }));
|
|
58
|
+
if (door.json)
|
|
59
|
+
return emitAgentDoor(door, { ok: true, ...body });
|
|
60
|
+
if (body.settings)
|
|
61
|
+
writeSettings(door, body.settings);
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
@@ -163,4 +163,38 @@ function reasonWord(reason, budget) {
|
|
|
163
163
|
if (safe.length === 0)
|
|
164
164
|
return ABSENT;
|
|
165
165
|
return safe.slice(0, budget);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Ticket 21952: the per-tick SIZE BUDGET's hold, the second token this file owns.
|
|
169
|
+
*
|
|
170
|
+
* A tick that sent some evidence and held the rest for the next tick reports
|
|
171
|
+
* `ok` with this token instead of failing, because overflow defers and converges
|
|
172
|
+
* by design. The collector writes it and the fleet board reads it
|
|
173
|
+
* (`apps/dashboard/src/lib/ops/fleet-budget-hold.ts`), which turns a hold that
|
|
174
|
+
* never drains into a RED row, so a growing backlog cannot stay green.
|
|
175
|
+
*
|
|
176
|
+
* held_budget:<held objects>:<held MiB>:<sent MiB>
|
|
177
|
+
* held_budget:3:901:2043
|
|
178
|
+
*/
|
|
179
|
+
export const BUDGET_HOLD_NOTICE_PREFIX = "held_budget";
|
|
180
|
+
const MIB = 1024 * 1024;
|
|
181
|
+
/** Render the size-budget hold, or null for a hold of nothing. Held MiB rounds up so a held byte never reads as 0. */
|
|
182
|
+
export function formatBudgetHoldNotice(use) {
|
|
183
|
+
if (use.deferredObjects <= 0)
|
|
184
|
+
return null;
|
|
185
|
+
const heldMib = Math.ceil(Math.max(0, use.deferredBytes) / MIB);
|
|
186
|
+
const sentMib = Math.round(Math.max(0, use.admittedBytes) / MIB);
|
|
187
|
+
return `${BUDGET_HOLD_NOTICE_PREFIX}:${Math.trunc(use.deferredObjects)}:${heldMib}:${sentMib}`;
|
|
188
|
+
}
|
|
189
|
+
/** Find a size-budget hold token anywhere in a receipt's detail. A search, for the same reason as `parseDeliveryHoldNotice`. */
|
|
190
|
+
export function parseBudgetHoldNotice(text) {
|
|
191
|
+
if (!text)
|
|
192
|
+
return null;
|
|
193
|
+
const match = new RegExp(String.raw `${BUDGET_HOLD_NOTICE_PREFIX}:(\d+):(\d+):(\d+)`, "u").exec(text);
|
|
194
|
+
if (!match)
|
|
195
|
+
return null;
|
|
196
|
+
const heldObjects = Number.parseInt(match[1], 10);
|
|
197
|
+
if (!Number.isFinite(heldObjects) || heldObjects <= 0)
|
|
198
|
+
return null;
|
|
199
|
+
return { heldObjects, heldMib: Number.parseInt(match[2], 10), sentMib: Number.parseInt(match[3], 10) };
|
|
166
200
|
}
|
package/dist/log-rotation.js
CHANGED
|
@@ -47,8 +47,8 @@ import path from "node:path";
|
|
|
47
47
|
*/
|
|
48
48
|
export const DEFAULT_LOG_ROTATION_MAX_BYTES = 10 * 1024 * 1024;
|
|
49
49
|
export const DEFAULT_LOG_ROTATION_KEEP = 2;
|
|
50
|
-
/** The
|
|
51
|
-
export const ROTATED_LOG_NAMES = ["sync.log", "sync.err.log"];
|
|
50
|
+
/** The files launchd writes for the scheduled ticks (sync, and BLI-21564 the reminder job). */
|
|
51
|
+
export const ROTATED_LOG_NAMES = ["sync.log", "sync.err.log", "remind.log", "remind.err.log"];
|
|
52
52
|
/**
|
|
53
53
|
* Caps the scheduled tick's own logs. Never throws: a rotation failure is data
|
|
54
54
|
* for the caller to log, never a reason a sync does not run.
|