@bli-cockpit/cli 0.2.244 → 0.2.246
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/delivery.js +37 -0
- package/dist/commands/issue-contracts.js +65 -4
- package/dist/commands/issue-list-page.js +34 -6
- package/dist/commands/issue-page-args.js +64 -0
- package/dist/commands/issue-relations.js +3 -2
- package/dist/commands/issue-show.js +13 -2
- package/dist/commands/issue-versions.js +5 -8
- package/dist/commands/issue-write.js +71 -16
- package/dist/commands/issue.js +22 -3
- package/dist/commands/local-args-tower-milestone.js +120 -0
- package/dist/commands/local-args-tower-work.js +44 -51
- package/dist/commands/local-args-tower.js +3 -1
- package/dist/commands/local-args.js +4 -1
- package/dist/commands/local-command-shapes-milestone.js +1 -0
- package/dist/commands/local-help-commands-tower-ops.js +8 -0
- package/dist/commands/local-help-commands-tower.js +6 -3
- package/dist/commands/local-help-milestone.js +23 -0
- package/dist/commands/local-help-project.js +1 -0
- package/dist/commands/local-help.js +4 -0
- package/dist/commands/local-run-ops.js +2 -0
- package/dist/commands/local.js +3 -0
- package/dist/commands/milestone.js +157 -0
- package/dist/commands/project-args.js +78 -2
- package/dist/commands/project.js +52 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/staging-gate.js +24 -3
- package/package.json +2 -2
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./agent-door.js";
|
|
2
|
+
import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
|
|
3
|
+
import { BODY_MAX_CHARS, READ_DEADLINE_MS, WRITE_DEADLINE_MS } from "./issue-contracts.js";
|
|
4
|
+
const TAG = "[milestone cli]";
|
|
5
|
+
export const milestoneLine = (m) => `${m.id} ${m.name} target ${m.target_date ?? "none"}${m.archived_at ? " [archived]" : ""} ${m.progress?.summary ?? ""}`.trimEnd();
|
|
6
|
+
function refuse(door, answer) {
|
|
7
|
+
const mutation = answer.body?.mutation;
|
|
8
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail, mutation ? { mutation } : {});
|
|
9
|
+
}
|
|
10
|
+
function projectQuery(command) {
|
|
11
|
+
return command.project ? `?project=${encodeURIComponent(command.project)}` : "";
|
|
12
|
+
}
|
|
13
|
+
/** `--description -` reads the text from stdin; anything else is the text itself. */
|
|
14
|
+
async function readDescription(value, io) {
|
|
15
|
+
if (value !== "-")
|
|
16
|
+
return { ok: true, text: value };
|
|
17
|
+
if (isInteractiveStdin(io))
|
|
18
|
+
return { ok: false, detail: "--description - reads the text from stdin, and nothing is piped in." };
|
|
19
|
+
try {
|
|
20
|
+
return { ok: true, text: await readPipedText(io.stdin, { maxChars: BODY_MAX_CHARS, overflowMessage: `A milestone description is limited to ${BODY_MAX_CHARS} characters.` }) };
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
return { ok: false, detail: error instanceof Error ? error.message : String(error) };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function runMilestone(command, io) {
|
|
27
|
+
const door = await openAgentDoor("milestone", command, io);
|
|
28
|
+
const ref = command.ref ? encodeURIComponent(command.ref) : "";
|
|
29
|
+
const writeIds = command.requestId ? { request_id: command.requestId } : {};
|
|
30
|
+
switch (command.action) {
|
|
31
|
+
case "list": {
|
|
32
|
+
const query = new URLSearchParams();
|
|
33
|
+
if (command.project)
|
|
34
|
+
query.set("project", command.project);
|
|
35
|
+
if (command.includeArchived)
|
|
36
|
+
query.set("include_archived", "true");
|
|
37
|
+
const answer = await askAgentDoor(door, {
|
|
38
|
+
path: `/api/work/milestones${query.size > 0 ? `?${query}` : ""}`, method: "GET", label: "milestone list", timeoutMs: READ_DEADLINE_MS,
|
|
39
|
+
});
|
|
40
|
+
if (!answer.ok)
|
|
41
|
+
return refuse(door, answer);
|
|
42
|
+
const body = answer.body;
|
|
43
|
+
const rows = body.milestones ?? [];
|
|
44
|
+
console.error(`${TAG} list done`, JSON.stringify({ returned: rows.length, coverage: body.coverage ?? null }));
|
|
45
|
+
if (door.json)
|
|
46
|
+
return emitAgentDoor(door, { ok: true, ...body });
|
|
47
|
+
if (rows.length === 0)
|
|
48
|
+
writeLine(door.io.stdout, "No milestones here. Create one with `cockpit milestone create --project <p> --name <n>`.");
|
|
49
|
+
for (const row of rows)
|
|
50
|
+
writeLine(door.io.stdout, milestoneLine(row));
|
|
51
|
+
if (body.coverage === "capped")
|
|
52
|
+
writeLine(door.io.stdout, "\nOnly the first 500 milestones are listed.");
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
case "show": {
|
|
56
|
+
const query = new URLSearchParams();
|
|
57
|
+
if (command.project)
|
|
58
|
+
query.set("project", command.project);
|
|
59
|
+
if (command.limit !== undefined)
|
|
60
|
+
query.set("limit", String(command.limit));
|
|
61
|
+
if (command.cursor)
|
|
62
|
+
query.set("cursor", command.cursor);
|
|
63
|
+
const answer = await askAgentDoor(door, {
|
|
64
|
+
path: `/api/work/milestones/${ref}${query.size > 0 ? `?${query}` : ""}`, method: "GET", label: "milestone show", timeoutMs: READ_DEADLINE_MS,
|
|
65
|
+
});
|
|
66
|
+
if (!answer.ok)
|
|
67
|
+
return refuse(door, answer);
|
|
68
|
+
const body = answer.body;
|
|
69
|
+
console.error(`${TAG} show done`, JSON.stringify({ milestone_id: body.milestone.id, members: body.issues?.length ?? 0, has_more: Boolean(body.has_more) }));
|
|
70
|
+
if (door.json)
|
|
71
|
+
return emitAgentDoor(door, { ok: true, ...body });
|
|
72
|
+
const out = door.io.stdout;
|
|
73
|
+
writeLine(out, milestoneLine(body.milestone));
|
|
74
|
+
writeLine(out, `project ${body.project?.name ?? body.milestone.project_id} updated ${body.milestone.updated_at ?? ""}`);
|
|
75
|
+
if (body.milestone.description)
|
|
76
|
+
writeLine(out, `\n${body.milestone.description}`);
|
|
77
|
+
writeLine(out, `\nProgress: ${body.progress?.summary ?? "no progress summary"}`);
|
|
78
|
+
for (const issue of body.issues ?? [])
|
|
79
|
+
writeLine(out, `${(issue.identifier ?? "").padEnd(9)} ${(issue.state ?? "").padEnd(11)} ${issue.title ?? ""}`);
|
|
80
|
+
if (body.has_more && body.next_cursor)
|
|
81
|
+
writeLine(out, `More tickets: --cursor ${body.next_cursor}`);
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
case "create":
|
|
85
|
+
case "update": {
|
|
86
|
+
const description = await readDescription(command.description, io);
|
|
87
|
+
if (!description.ok)
|
|
88
|
+
return failAgentDoor(door, TAG, "invalid_body", description.detail);
|
|
89
|
+
const payload = {
|
|
90
|
+
...writeIds,
|
|
91
|
+
...(command.action === "create" ? { project: command.project } : {}),
|
|
92
|
+
...(command.action === "update" && command.expectedUpdatedAt ? { expected_updated_at: command.expectedUpdatedAt } : {}),
|
|
93
|
+
...(command.name === undefined ? {} : { name: command.name }),
|
|
94
|
+
...(description.text === undefined ? {} : { description: description.text }),
|
|
95
|
+
...(command.targetDate === undefined ? {} : { target_date: command.targetDate }),
|
|
96
|
+
};
|
|
97
|
+
return await write(command, door, command.action === "create"
|
|
98
|
+
? { path: "/api/work/milestones", method: "POST", body: payload }
|
|
99
|
+
: { path: `/api/work/milestones/${ref}${projectQuery(command)}`, method: "PATCH", body: payload });
|
|
100
|
+
}
|
|
101
|
+
case "archive":
|
|
102
|
+
case "restore":
|
|
103
|
+
return await write(command, door, {
|
|
104
|
+
path: `/api/work/milestones/${ref}${projectQuery(command)}`,
|
|
105
|
+
method: "PATCH",
|
|
106
|
+
body: { ...writeIds, ...(command.expectedUpdatedAt ? { expected_updated_at: command.expectedUpdatedAt } : {}), archived: command.action === "archive" },
|
|
107
|
+
});
|
|
108
|
+
case "add":
|
|
109
|
+
case "remove":
|
|
110
|
+
return await membership(command, door, ref, writeIds);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function write(command, door, req) {
|
|
114
|
+
const answer = await askAgentDoor(door, { ...req, label: `milestone ${command.action}`, timeoutMs: WRITE_DEADLINE_MS });
|
|
115
|
+
if (!answer.ok)
|
|
116
|
+
return refuse(door, answer);
|
|
117
|
+
const body = answer.body;
|
|
118
|
+
console.error(`${TAG} ${command.action} committed`, JSON.stringify({ milestone_id: body.milestone.id, request_id: body.mutation?.request_id ?? null, delivery: body.mutation?.delivery?.status ?? null }));
|
|
119
|
+
if (door.json)
|
|
120
|
+
return emitAgentDoor(door, { ok: true, ...body });
|
|
121
|
+
const label = { create: "Created", update: "Updated", archive: "Archived", restore: "Restored" }[command.action] ?? "Saved";
|
|
122
|
+
writeLine(door.io.stdout, `${label}: ${milestoneLine(body.milestone)}`);
|
|
123
|
+
writeLine(door.io.stdout, `Request ID ${body.mutation?.request_id ?? "?"}, delivery ${body.mutation?.delivery?.status ?? "tower_only"} (milestones never reach Linear).`);
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
async function membership(command, door, ref, writeIds) {
|
|
127
|
+
const answer = await askAgentDoor(door, {
|
|
128
|
+
path: `/api/work/milestones/${ref}/issues${projectQuery(command)}`,
|
|
129
|
+
method: "POST",
|
|
130
|
+
label: `milestone ${command.action}`,
|
|
131
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
132
|
+
body: { ...writeIds, [command.action]: command.tickets },
|
|
133
|
+
});
|
|
134
|
+
// 207 means some items were refused: still a readable answer with every receipt.
|
|
135
|
+
const body = (("body" in answer ? answer.body : undefined) ?? {});
|
|
136
|
+
if (!answer.ok && !Array.isArray(body.items))
|
|
137
|
+
return refuse(door, answer);
|
|
138
|
+
const items = body.items ?? [];
|
|
139
|
+
console.error(`${TAG} ${command.action} done`, JSON.stringify({
|
|
140
|
+
request_id: body.request_id ?? null, asked: items.length, committed: body.committed ?? 0, refused: body.refused ?? 0, skipped: body.skipped ?? 0, unknown: body.unknown ?? 0,
|
|
141
|
+
}));
|
|
142
|
+
const failed = (body.refused ?? 0) + (body.unknown ?? 0) > 0;
|
|
143
|
+
if (door.json) {
|
|
144
|
+
emitAgentDoor(door, { ok: !failed, ...body });
|
|
145
|
+
return failed ? 1 : 0;
|
|
146
|
+
}
|
|
147
|
+
for (const item of items) {
|
|
148
|
+
const linear = item.delivery?.status ? ` Linear copy: ${item.delivery.status}${item.delivery.reason ? ` (${item.delivery.reason})` : ""}` : "";
|
|
149
|
+
writeLine(door.io.stdout, `${(item.identifier ?? item.ref).padEnd(9)} ${item.action} ${item.outcome}${item.reason ? ` ${item.reason}` : ""}${linear}`);
|
|
150
|
+
if (item.message && item.outcome !== "committed")
|
|
151
|
+
writeLine(door.io.stdout, ` ${item.message}`);
|
|
152
|
+
}
|
|
153
|
+
writeLine(door.io.stdout, `\n${body.committed ?? 0} committed, ${body.refused ?? 0} refused, ${body.skipped ?? 0} skipped, ${body.unknown ?? 0} unknown. Request ID ${body.request_id ?? "?"} (retry with it to replay).`);
|
|
154
|
+
if (body.progress?.summary)
|
|
155
|
+
writeLine(door.io.stdout, `Progress: ${body.progress.summary}`);
|
|
156
|
+
return failed ? 1 : 0;
|
|
157
|
+
}
|
|
@@ -4,20 +4,94 @@
|
|
|
4
4
|
* A description never travels on argv by default: `--file <path>` reads it from a local file (safest on Windows);
|
|
5
5
|
* `--description <text>` is the short form and `--description none` clears it. Write-safety flags
|
|
6
6
|
* (`--request-id`, `--expected-updated-at`) belong to the four write verbs.
|
|
7
|
+
* BLI-22360: create/update also take the planning flags (`--lead`, `--member`, `--add-member`, `--remove-member`,
|
|
8
|
+
* `--status`, `--priority`, `--start`, `--target`, their `--*-resolution`, `--content-file` / `--content`); `none` clears.
|
|
7
9
|
*/
|
|
8
10
|
import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
9
11
|
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
10
12
|
const ACTIONS = ["list", "show", "create", "update", "archive", "restore"];
|
|
11
13
|
const WRITES = ["create", "update", "archive", "restore"];
|
|
14
|
+
/** BLI-22360: the planning fields of `project create|update`. `none` clears a value (update). */
|
|
15
|
+
const PLANNING_FLAGS = [
|
|
16
|
+
"--lead", "--member", "--add-member", "--remove-member", "--status", "--priority",
|
|
17
|
+
"--start", "--start-resolution", "--target", "--target-resolution", "--content-file", "--content",
|
|
18
|
+
];
|
|
19
|
+
const DAY = /^\d{4}-\d{2}-\d{2}$/;
|
|
20
|
+
const PRIORITIES = ["none", "urgent", "high", "medium", "low", "0", "1", "2", "3", "4"];
|
|
21
|
+
const STATUSES = ["backlog", "planned", "started", "in-progress", "in_progress", "paused", "completed", "done", "canceled", "none"];
|
|
22
|
+
const RESOLUTIONS = ["day", "month", "quarter", "halfYear", "year", "none"];
|
|
23
|
+
/** `--member a@x --member b@x` or `--member a@x,b@x`; `none` alone clears (sent as an empty set). */
|
|
24
|
+
function people(values, flag) {
|
|
25
|
+
const raw = values.flagValues.get(flag) ?? (values.flags.has(flag) ? [values.flags.get(flag)] : undefined);
|
|
26
|
+
if (raw === undefined)
|
|
27
|
+
return undefined;
|
|
28
|
+
const list = raw.flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean);
|
|
29
|
+
if (list.length === 0)
|
|
30
|
+
throw new Error(`${flag} needs an email, a user id or me.`);
|
|
31
|
+
return list;
|
|
32
|
+
}
|
|
33
|
+
function day(flag, value) {
|
|
34
|
+
if (value === undefined)
|
|
35
|
+
return undefined;
|
|
36
|
+
if (value.trim().toLowerCase() === "none")
|
|
37
|
+
return null;
|
|
38
|
+
if (!DAY.test(value) || new Date(`${value}T12:00:00Z`).toISOString().slice(0, 10) !== value) {
|
|
39
|
+
throw new Error(`${flag} must be a calendar date as YYYY-MM-DD, or none.`);
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
function oneOf(flag, value, allowed) {
|
|
44
|
+
if (value === undefined)
|
|
45
|
+
return undefined;
|
|
46
|
+
if (!allowed.includes(value.trim()))
|
|
47
|
+
throw new Error(`${flag} must be one of: ${allowed.join(", ")}.`);
|
|
48
|
+
return value.trim();
|
|
49
|
+
}
|
|
50
|
+
function readPlanning(values, action) {
|
|
51
|
+
const given = PLANNING_FLAGS.filter((flag) => values.flags.has(flag));
|
|
52
|
+
if (given.length === 0)
|
|
53
|
+
return {};
|
|
54
|
+
if (action !== "create" && action !== "update")
|
|
55
|
+
throw new Error(`${given[0]} belongs to project create and update.`);
|
|
56
|
+
if (values.flags.has("--content") && values.flags.has("--content-file"))
|
|
57
|
+
throw new Error("Give --content or --content-file, not both.");
|
|
58
|
+
const members = people(values, "--member");
|
|
59
|
+
const addMembers = people(values, "--add-member");
|
|
60
|
+
const removeMembers = people(values, "--remove-member");
|
|
61
|
+
if (members && (addMembers || removeMembers))
|
|
62
|
+
throw new Error("Give --member (the whole set) or --add-member / --remove-member, not both.");
|
|
63
|
+
if (action === "create" && (addMembers || removeMembers))
|
|
64
|
+
throw new Error("project create takes --member; --add-member and --remove-member belong to update.");
|
|
65
|
+
const lead = values.flags.get("--lead");
|
|
66
|
+
const content = values.flags.get("--content");
|
|
67
|
+
const status = oneOf("--status", values.flags.get("--status"), STATUSES);
|
|
68
|
+
const priority = oneOf("--priority", values.flags.get("--priority"), PRIORITIES);
|
|
69
|
+
return {
|
|
70
|
+
...(lead === undefined ? {} : { lead: lead.trim().toLowerCase() === "none" ? null : lead.trim() }),
|
|
71
|
+
...(members === undefined ? {} : { members }),
|
|
72
|
+
...(addMembers === undefined ? {} : { addMembers }),
|
|
73
|
+
...(removeMembers === undefined ? {} : { removeMembers }),
|
|
74
|
+
...(status === undefined ? {} : { status: status === "none" ? null : status }),
|
|
75
|
+
...(priority === undefined ? {} : { priority }),
|
|
76
|
+
...(values.flags.has("--start") ? { startDate: day("--start", values.flags.get("--start")) } : {}),
|
|
77
|
+
...(values.flags.has("--target") ? { targetDate: day("--target", values.flags.get("--target")) } : {}),
|
|
78
|
+
...(values.flags.has("--start-resolution") ? { startDateResolution: oneOf("--start-resolution", values.flags.get("--start-resolution"), RESOLUTIONS) } : {}),
|
|
79
|
+
...(values.flags.has("--target-resolution") ? { targetDateResolution: oneOf("--target-resolution", values.flags.get("--target-resolution"), RESOLUTIONS) } : {}),
|
|
80
|
+
...(content === undefined ? {} : { content: content.trim().toLowerCase() === "none" ? null : content }),
|
|
81
|
+
...(values.flags.has("--content-file") ? { contentFilePath: optionalNonEmpty(values.flags.get("--content-file")) } : {}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
12
84
|
export function parseProjectArgs(args) {
|
|
13
85
|
const values = parseNamedArgs(args, {
|
|
14
86
|
allowedFlags: [
|
|
15
87
|
"--home", "--dashboard-url", "--archived", "--cursor", "--limit", "--json",
|
|
16
88
|
"--name", "--description", "--file", "--color", "--request-id", "--expected-updated-at",
|
|
89
|
+
...PLANNING_FLAGS,
|
|
17
90
|
],
|
|
18
91
|
valueFlags: [
|
|
19
92
|
"--home", "--dashboard-url", "--cursor", "--limit",
|
|
20
93
|
"--name", "--description", "--file", "--color", "--request-id", "--expected-updated-at",
|
|
94
|
+
...PLANNING_FLAGS,
|
|
21
95
|
],
|
|
22
96
|
});
|
|
23
97
|
const first = values.positionals[0];
|
|
@@ -59,8 +133,9 @@ export function parseProjectArgs(args) {
|
|
|
59
133
|
const rawDescription = values.flags.get("--description");
|
|
60
134
|
const description = rawDescription === undefined ? undefined : rawDescription.trim().toLowerCase() === "none" ? null : rawDescription;
|
|
61
135
|
const filePath = optionalNonEmpty(values.flags.get("--file"));
|
|
62
|
-
|
|
63
|
-
|
|
136
|
+
const planning = readPlanning(values, action);
|
|
137
|
+
if (action === "update" && name === undefined && description === undefined && filePath === undefined && Object.keys(planning).length === 0) {
|
|
138
|
+
throw new Error("project update needs --name, --description (or none), --file, or a planning flag (--lead, --member, --status, --priority, --start, --target, --content-file).");
|
|
64
139
|
}
|
|
65
140
|
return {
|
|
66
141
|
kind: "project",
|
|
@@ -77,6 +152,7 @@ export function parseProjectArgs(args) {
|
|
|
77
152
|
...(optionalNonEmpty(values.flags.get("--color")) ? { color: optionalNonEmpty(values.flags.get("--color")) } : {}),
|
|
78
153
|
...(requestId ? { requestId } : {}),
|
|
79
154
|
...(expectedUpdatedAt ? { expectedUpdatedAt } : {}),
|
|
155
|
+
...(Object.keys(planning).length > 0 ? { planning } : {}),
|
|
80
156
|
json: values.booleans.has("--json"),
|
|
81
157
|
};
|
|
82
158
|
}
|
package/dist/commands/project.js
CHANGED
|
@@ -12,6 +12,22 @@ import { pageFieldsOf, pageFooter } from "./issue-list-page.js";
|
|
|
12
12
|
import { readBody } from "./issue-contracts.js";
|
|
13
13
|
const TAG = "[project cli]";
|
|
14
14
|
const READ_DEADLINE_MS = 30_000;
|
|
15
|
+
const PRIORITY_WORDS = ["none", "urgent", "high", "medium", "low"];
|
|
16
|
+
const person = (p) => p.name && p.email ? `${p.name} <${p.email}>` : (p.email ?? p.name ?? p.user_id);
|
|
17
|
+
/** "2026-06-01 (month)" for a period, the bare day for an exact date. */
|
|
18
|
+
const dated = (day, resolution) => day ? `${day}${resolution ? ` (${resolution})` : ""}` : "none";
|
|
19
|
+
/** The planning block `project show` prints (BLI-22360). */
|
|
20
|
+
export function planningLines(body) {
|
|
21
|
+
const project = body.project;
|
|
22
|
+
if (!project)
|
|
23
|
+
return [];
|
|
24
|
+
return [
|
|
25
|
+
`status ${project.status ?? "none"} priority ${PRIORITY_WORDS[project.priority ?? 0] ?? project.priority} start ${dated(project.start_date, project.start_date_resolution)} target ${dated(project.target_date, project.target_date_resolution)}`,
|
|
26
|
+
`lead ${body.lead ? person(body.lead) : "none"}`,
|
|
27
|
+
`members ${(body.members ?? []).length === 0 ? "none" : (body.members ?? []).map(person).join(", ")}`,
|
|
28
|
+
...(body.milestones ?? []).map((m) => `milestone ${m.name}${m.archived_at ? " (archived)" : ""} target ${m.target_date ?? "none"} ${m.progress?.summary ?? ""} ${m.id}`),
|
|
29
|
+
];
|
|
30
|
+
}
|
|
15
31
|
export async function runProject(command, io) {
|
|
16
32
|
const door = await openAgentDoor("project", command, io);
|
|
17
33
|
if (command.action !== "list")
|
|
@@ -65,6 +81,10 @@ async function runProjectVerb(command, door) {
|
|
|
65
81
|
writeLine(out, `id ${project?.id ?? ""} updated ${project?.updated_at ?? ""}`);
|
|
66
82
|
if (project?.description)
|
|
67
83
|
writeLine(out, project.description);
|
|
84
|
+
for (const line of planningLines(body))
|
|
85
|
+
writeLine(out, line);
|
|
86
|
+
if (project?.content)
|
|
87
|
+
writeLine(out, `\n${project.content}\n`);
|
|
68
88
|
for (const issue of body.issues ?? [])
|
|
69
89
|
writeLine(out, `${issue.identifier ?? ""} ${issue.state ?? ""} ${issue.title ?? ""}`);
|
|
70
90
|
const footer = pageFooter(body, "tickets");
|
|
@@ -92,6 +112,38 @@ async function runProjectVerb(command, door) {
|
|
|
92
112
|
else if (command.description !== undefined) {
|
|
93
113
|
payload["description"] = command.description;
|
|
94
114
|
}
|
|
115
|
+
const planning = command.planning;
|
|
116
|
+
if (planning) {
|
|
117
|
+
if (planning.lead !== undefined)
|
|
118
|
+
payload["lead"] = planning.lead;
|
|
119
|
+
if (planning.members !== undefined)
|
|
120
|
+
payload["members"] = planning.members;
|
|
121
|
+
if (planning.addMembers !== undefined)
|
|
122
|
+
payload["add_members"] = planning.addMembers;
|
|
123
|
+
if (planning.removeMembers !== undefined)
|
|
124
|
+
payload["remove_members"] = planning.removeMembers;
|
|
125
|
+
if (planning.status !== undefined)
|
|
126
|
+
payload["status"] = planning.status;
|
|
127
|
+
if (planning.priority !== undefined)
|
|
128
|
+
payload["priority"] = planning.priority;
|
|
129
|
+
if (planning.startDate !== undefined)
|
|
130
|
+
payload["start_date"] = planning.startDate;
|
|
131
|
+
if (planning.targetDate !== undefined)
|
|
132
|
+
payload["target_date"] = planning.targetDate;
|
|
133
|
+
if (planning.startDateResolution !== undefined)
|
|
134
|
+
payload["start_date_resolution"] = planning.startDateResolution;
|
|
135
|
+
if (planning.targetDateResolution !== undefined)
|
|
136
|
+
payload["target_date_resolution"] = planning.targetDateResolution;
|
|
137
|
+
if (planning.contentFilePath) {
|
|
138
|
+
const read = await readBody({ filePath: planning.contentFilePath }, door.io);
|
|
139
|
+
if (!read.ok)
|
|
140
|
+
return failAgentDoor(door, TAG, read.reason, read.detail);
|
|
141
|
+
payload["content"] = read.text;
|
|
142
|
+
}
|
|
143
|
+
else if (planning.content !== undefined) {
|
|
144
|
+
payload["content"] = planning.content;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
95
147
|
}
|
|
96
148
|
const path = command.action === "create" ? base : command.action === "update" ? ref : `${ref}/${command.action}`;
|
|
97
149
|
const answer = await askAgentDoor(door, {
|
|
@@ -21,7 +21,7 @@ export async function runCockpitCli(argv, io) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
if (command === "--version" || command === "-V" || command === "version") {
|
|
24
|
-
writeLine(io?.stdout ?? process.stdout, "0.2.
|
|
24
|
+
writeLine(io?.stdout ?? process.stdout, "0.2.246");
|
|
25
25
|
return 0;
|
|
26
26
|
}
|
|
27
27
|
|
package/dist/staging-gate.js
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
*
|
|
16
16
|
* Three reasons close the gate, most urgent first:
|
|
17
17
|
*
|
|
18
|
-
* - `low_disk`: the volume's free space is under max(5 GiB, 5% of the volume)
|
|
18
|
+
* - `low_disk`: the volume's free space is under max(5 GiB, 5% of the volume),
|
|
19
|
+
* for a file bigger than 1% of the free space left (smaller files are admitted).
|
|
19
20
|
* - `state_over_ceiling`: everything the collector writes under its state dir
|
|
20
21
|
* (staging, spool, logs, cursors, state; never the study folders
|
|
21
22
|
* `state-dir-audit.ts` recognises) would pass the hard ceiling, 512 MiB by
|
|
@@ -43,6 +44,12 @@ export const STATE_CEILING_ENV = "COCKPIT_STATE_CEILING_BYTES";
|
|
|
43
44
|
export const MIN_FREE_ENV = "COCKPIT_MIN_FREE_BYTES";
|
|
44
45
|
export const LOW_DISK_FLOOR_BYTES = 5 * 1024 * 1024 * 1024;
|
|
45
46
|
export const LOW_DISK_FLOOR_FRACTION = 0.05;
|
|
47
|
+
/**
|
|
48
|
+
* A file at most this fraction of the free space left is never refused for low
|
|
49
|
+
* disk: a 7 MB upload does not threaten a volume with gigabytes free, even one
|
|
50
|
+
* under the floor. Only a copy that would eat a real share of what is left waits.
|
|
51
|
+
*/
|
|
52
|
+
export const LOW_DISK_SMALL_FILE_FRACTION = 0.01;
|
|
46
53
|
/**
|
|
47
54
|
* Each pack's `manifest.json` is staged beside the payload and its size grows
|
|
48
55
|
* with the gap ledger, so it is not known when a file is admitted. A tenth of
|
|
@@ -174,9 +181,22 @@ export function admitToStaging(allowance, byteLength, options = {}) {
|
|
|
174
181
|
const spent = allowance.admitted_bytes;
|
|
175
182
|
const stagedNow = snapshot.staging_bytes + spent;
|
|
176
183
|
let reason = null;
|
|
177
|
-
|
|
184
|
+
const freeNow = snapshot.free_bytes === null ? null : snapshot.free_bytes - spent;
|
|
185
|
+
const belowFloor = freeNow !== null &&
|
|
178
186
|
snapshot.floor_bytes !== null &&
|
|
179
|
-
|
|
187
|
+
freeNow - byteLength < snapshot.floor_bytes;
|
|
188
|
+
const smallForFree = freeNow !== null && freeNow > 0 && byteLength <= freeNow * LOW_DISK_SMALL_FILE_FRACTION;
|
|
189
|
+
if (belowFloor && smallForFree) {
|
|
190
|
+
console.error("[staging-gate] low-disk floor waived, file is under 1% of free space", JSON.stringify({
|
|
191
|
+
event: STAGING_DEFERRED,
|
|
192
|
+
reason: "low_disk_waived_small_file",
|
|
193
|
+
kind: options.kind ?? null,
|
|
194
|
+
file_bytes: byteLength,
|
|
195
|
+
free_bytes: freeNow,
|
|
196
|
+
floor_bytes: snapshot.floor_bytes,
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
if (belowFloor && !smallForFree) {
|
|
180
200
|
reason = "low_disk";
|
|
181
201
|
}
|
|
182
202
|
else if (snapshot.collector_bytes + spent + byteLength > snapshot.ceiling_bytes) {
|
|
@@ -202,6 +222,7 @@ export function admitToStaging(allowance, byteLength, options = {}) {
|
|
|
202
222
|
reason,
|
|
203
223
|
kind: options.kind ?? null,
|
|
204
224
|
file_bytes: byteLength,
|
|
225
|
+
needed_bytes: byteLength,
|
|
205
226
|
staging_bytes: snapshot.staging_bytes,
|
|
206
227
|
collector_bytes: snapshot.collector_bytes,
|
|
207
228
|
free_bytes: snapshot.free_bytes,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.246",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@bli-cockpit/memory-mcp": "0.1.72",
|
|
31
|
-
"@bli-cockpit/mcp": "0.1.
|
|
31
|
+
"@bli-cockpit/mcp": "0.1.151",
|
|
32
32
|
"@bli-cockpit/telemetry-core": "0.1.79"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|