@bli-cockpit/cli 0.2.145 → 0.2.146

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.
@@ -11,29 +11,32 @@ import { askAgentDoor } from "./agent-door.js";
11
11
  import { writeLine } from "./cli-io.js";
12
12
  const TEAM_DIRECTORY_TIMEOUT_MS = 30_000;
13
13
  /**
14
- * One `/api/team/members` call however many issues are in hand. Best-effort:
15
- * a directory that cannot be read still lets the issue(s) print their bare
16
- * `assignee_id`, the pre-BLI-10450 behaviour, rather than failing the whole
17
- * read over an enrichment nobody asked to gate on.
14
+ * The `/api/team/members` call by itself. The response is the whole org
15
+ * roster either way — it never actually depended on which issues came back —
16
+ * so a caller with its own read already in flight (BLI-10579: `issue list`'s
17
+ * `/api/work/issues`) can start this one at the same time instead of after.
18
18
  */
19
- export async function resolveAssigneeDirectory(door, assigneeIds) {
20
- const wanted = new Set(assigneeIds.filter((id) => Boolean(id)));
21
- const directory = new Map();
22
- if (wanted.size === 0)
23
- return directory;
19
+ async function fetchTeamDirectory(door) {
24
20
  const answer = await askAgentDoor(door, {
25
21
  path: "/api/team/members",
26
22
  method: "GET",
27
23
  label: "issue assignee directory",
28
24
  timeoutMs: TEAM_DIRECTORY_TIMEOUT_MS,
29
25
  });
30
- if (!answer.ok) {
31
- writeLine(door.io.stderr, `[issue cli] assignee directory unread ${JSON.stringify({ reason: answer.reason, wanted: wanted.size })}`);
26
+ if (!answer.ok)
27
+ return { ok: false, people: [], reason: answer.reason };
28
+ return { ok: true, people: answer.body.people ?? [] };
29
+ }
30
+ function buildDirectory(door, fetched, wanted) {
31
+ const directory = new Map();
32
+ if (wanted.size === 0)
33
+ return directory;
34
+ if (!fetched.ok) {
35
+ writeLine(door.io.stderr, `[issue cli] assignee directory unread ${JSON.stringify({ reason: fetched.reason, wanted: wanted.size })}`);
32
36
  return directory;
33
37
  }
34
- const people = answer.body.people ?? [];
35
38
  let resolved = 0;
36
- for (const person of people) {
39
+ for (const person of fetched.people) {
37
40
  const userId = person.account?.userId;
38
41
  if (!userId || !wanted.has(userId))
39
42
  continue;
@@ -43,6 +46,36 @@ export async function resolveAssigneeDirectory(door, assigneeIds) {
43
46
  writeLine(door.io.stderr, `[issue cli] assignee directory resolved ${JSON.stringify({ wanted: wanted.size, resolved })}`);
44
47
  return directory;
45
48
  }
49
+ /**
50
+ * One `/api/team/members` call however many issues are in hand. Best-effort:
51
+ * a directory that cannot be read still lets the issue(s) print their bare
52
+ * `assignee_id`, the pre-BLI-10450 behaviour, rather than failing the whole
53
+ * read over an enrichment nobody asked to gate on. Skips the call entirely
54
+ * when nobody in the batch is assigned.
55
+ */
56
+ export async function resolveAssigneeDirectory(door, assigneeIds) {
57
+ const wanted = new Set(assigneeIds.filter((id) => Boolean(id)));
58
+ if (wanted.size === 0)
59
+ return new Map();
60
+ return buildDirectory(door, await fetchTeamDirectory(door), wanted);
61
+ }
62
+ /**
63
+ * BLI-10579: start the directory read now, before the assignee ids that will
64
+ * eventually filter it are even known — its network call never needed them.
65
+ * Pair with `finishAssigneeDirectory` once the caller's own request answers.
66
+ */
67
+ export function beginAssigneeDirectoryFetch(door) {
68
+ return fetchTeamDirectory(door);
69
+ }
70
+ /**
71
+ * Always awaits the in-flight fetch, even when `wanted` turns out empty —
72
+ * it started eagerly, before that was knowable, so there is no unwaited
73
+ * background call left dangling past the command's exit.
74
+ */
75
+ export async function finishAssigneeDirectory(door, fetchInFlight, assigneeIds) {
76
+ const wanted = new Set(assigneeIds.filter((id) => Boolean(id)));
77
+ return buildDirectory(door, await fetchInFlight, wanted);
78
+ }
46
79
  /** `assignee_name`/`assignee_email` alongside the bare id, for JSON output. */
47
80
  export function withAssigneeIdentity(row, directory) {
48
81
  const identity = row.assignee_id ? directory.get(row.assignee_id) : undefined;
@@ -27,7 +27,7 @@
27
27
  */
28
28
  import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
29
29
  import { writeLine } from "./cli-io.js";
30
- import { assigneeLabel, resolveAssigneeDirectory, withAssigneeIdentity } from "./issue-directory.js";
30
+ import { assigneeLabel, beginAssigneeDirectoryFetch, finishAssigneeDirectory, withAssigneeIdentity } from "./issue-directory.js";
31
31
  import { showIssue } from "./issue-show.js";
32
32
  import { READ_DEADLINE_MS, TAG, resolveProjectId, sizeForEstimate, withSizeLabel, } from "./issue-contracts.js";
33
33
  import { commentIssue, createIssue, moveIssue, updateIssue } from "./issue-write.js";
@@ -105,14 +105,24 @@ async function listIssues(command, door) {
105
105
  query.set("project_id", resolved.id);
106
106
  }
107
107
  const suffix = query.toString();
108
+ // BLI-10579: the assignee directory (`/api/team/members`) is the whole org
109
+ // roster regardless of which issues come back — it never actually depended
110
+ // on the issue list — so start it now, alongside the issues request,
111
+ // instead of waiting for that answer first (measured ~580ms of the ~580ms
112
+ // `work list` p50 was exactly this serial second call).
113
+ const directoryFetch = beginAssigneeDirectoryFetch(door);
108
114
  const answer = await askAgentDoor(door, {
109
115
  path: `/api/work/issues${suffix ? `?${suffix}` : ""}`,
110
116
  method: "GET",
111
117
  label: "issue list",
112
118
  timeoutMs: READ_DEADLINE_MS,
113
119
  });
114
- if (!answer.ok)
120
+ if (!answer.ok) {
121
+ // Not awaited for its own sake — `finishAssigneeDirectory` always awaits
122
+ // the in-flight fetch so it never leaks past this command's exit.
123
+ await finishAssigneeDirectory(door, directoryFetch, []);
115
124
  return failAgentDoor(door, TAG, answer.reason, answer.detail);
125
+ }
116
126
  const body = answer.body;
117
127
  const issues = body.issues ?? [];
118
128
  // BLI-10447: the gaps go to stderr in both modes, so `--json` stdout stays one object.
@@ -121,7 +131,7 @@ async function listIssues(command, door) {
121
131
  // BLI-10450: one extra call for the whole list, never one per row — the
122
132
  // uuid `assignee_id` alone forced a second `cockpit team members` call and
123
133
  // a join done by hand to answer "who owns what".
124
- const directory = await resolveAssigneeDirectory(door, issues.map((issue) => issue.assignee_id));
134
+ const directory = await finishAssigneeDirectory(door, directoryFetch, issues.map((issue) => issue.assignee_id));
125
135
  if (door.json) {
126
136
  return emitAgentDoor(door, {
127
137
  ok: true,
@@ -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.145");
24
+ writeLine(io?.stdout ?? process.stdout, "0.2.146");
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.145",
3
+ "version": "0.2.146",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {