@bli-cockpit/cli 0.2.244 → 0.2.245

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.
@@ -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
- if (action === "update" && name === undefined && description === undefined && filePath === undefined) {
63
- throw new Error("project update needs --name, --description (or none) or --file.");
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
  }
@@ -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.244");
24
+ writeLine(io?.stdout ?? process.stdout, "0.2.245");
25
25
  return 0;
26
26
  }
27
27
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.244",
3
+ "version": "0.2.245",
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.149",
31
+ "@bli-cockpit/mcp": "0.1.150",
32
32
  "@bli-cockpit/telemetry-core": "0.1.79"
33
33
  },
34
34
  "repository": {