@bli-cockpit/cli 0.2.4 → 0.2.7

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.
@@ -1,12 +1,13 @@
1
1
  import path from "node:path";
2
- export function collapseAncestorRoots(roots) {
2
+ export function collapseAncestorRoots(roots, pathApi = path) {
3
3
  const collapsed = [];
4
4
  for (const root of roots) {
5
- const resolved = path.resolve(root);
6
- if (collapsed.some((existing) => containsPath(existing, resolved)))
5
+ const resolved = pathApi.resolve(root);
6
+ if (collapsed.some((existing) => containsPath(existing, resolved, pathApi))) {
7
7
  continue;
8
+ }
8
9
  for (let index = collapsed.length - 1; index >= 0; index -= 1) {
9
- if (containsPath(resolved, collapsed[index])) {
10
+ if (containsPath(resolved, collapsed[index], pathApi)) {
10
11
  collapsed.splice(index, 1);
11
12
  }
12
13
  }
@@ -14,21 +15,33 @@ export function collapseAncestorRoots(roots) {
14
15
  }
15
16
  return collapsed;
16
17
  }
17
- export function normalizeCollectionRoots(roots) {
18
- const collapsed = collapseAncestorRoots(roots);
19
- if (!collapsed.some(isBliWorkspaceRoot))
18
+ export function normalizeCollectionRoots(roots, pathApi = path) {
19
+ const collapsed = collapseAncestorRoots(roots, pathApi);
20
+ if (!collapsed.some((root) => isBliWorkspaceRoot(root, pathApi))) {
20
21
  return collapsed;
21
- return collapsed.filter((root) => !isCodexWorktreePath(root));
22
+ }
23
+ return collapsed.filter((root) => !isCodexWorktreePath(root, pathApi));
22
24
  }
23
- function containsPath(parent, candidate) {
24
- const relative = path.relative(parent, candidate);
25
+ export function containsPath(parent, candidate, pathApi = path) {
26
+ const relative = pathApi.relative(parent, candidate);
25
27
  return (relative === "" ||
26
- (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative)));
28
+ (!!relative &&
29
+ relative !== ".." &&
30
+ !relative.startsWith(`..${pathApi.sep}`) &&
31
+ !pathApi.isAbsolute(relative)));
32
+ }
33
+ export function isSamePath(left, right, pathApi = path) {
34
+ return pathApi.relative(left, right) === "";
35
+ }
36
+ function comparableSegment(value, pathApi) {
37
+ return pathApi.sep === "\\" ? value.toLowerCase() : value;
27
38
  }
28
- function isBliWorkspaceRoot(root) {
29
- return path.basename(root) === "BLI";
39
+ function isBliWorkspaceRoot(root, pathApi) {
40
+ return comparableSegment(pathApi.basename(root), pathApi) ===
41
+ comparableSegment("BLI", pathApi);
30
42
  }
31
- function isCodexWorktreePath(root) {
32
- const parts = root.split(path.sep).filter(Boolean);
33
- return parts.some((part, index) => part === ".codex" && parts[index + 1] === "worktrees");
43
+ export function isCodexWorktreePath(root, pathApi = path) {
44
+ const parts = root.split(pathApi.sep).filter(Boolean);
45
+ return parts.some((part, index) => comparableSegment(part, pathApi) === ".codex" &&
46
+ comparableSegment(parts[index + 1] ?? "", pathApi) === "worktrees");
34
47
  }
@@ -0,0 +1,191 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ const OUTBOX_DIRECTORY = "install-events";
5
+ const MAX_PENDING_ENTRIES = 100;
6
+ const MAX_EVENTS_PER_ENTRY = 40;
7
+ export async function enqueueInstallEventEntry(paths, options) {
8
+ if (options.events.length === 0)
9
+ return null;
10
+ const createdAt = (options.now ?? new Date()).toISOString();
11
+ const entry = {
12
+ schema_version: "cockpit-install-event-outbox.v1",
13
+ outbox_id: `install-event-${crypto.randomUUID()}`,
14
+ created_at: createdAt,
15
+ last_attempt_at: null,
16
+ retry_count: 0,
17
+ last_failure_reason: null,
18
+ dashboard_url: options.dashboardUrl,
19
+ cli_version: options.cliVersion,
20
+ command: options.command,
21
+ os_platform: options.osPlatform,
22
+ events: options.events.slice(0, MAX_EVENTS_PER_ENTRY).map((event) => ({
23
+ step: event.step,
24
+ status: event.status,
25
+ ...(event.error_code ? { error_code: event.error_code } : {}),
26
+ at: event.at ?? createdAt,
27
+ })),
28
+ };
29
+ await writeEntry(paths, entry);
30
+ await pruneInstallEventOutbox(paths);
31
+ return entry;
32
+ }
33
+ export async function readPendingInstallEventEntries(paths) {
34
+ const directory = installEventOutboxDirectory(paths);
35
+ const names = await fs.readdir(directory).catch(() => []);
36
+ const entries = [];
37
+ for (const name of names.filter((candidate) => candidate.endsWith(".json"))) {
38
+ const filePath = path.join(directory, name);
39
+ try {
40
+ const parsed = parseEntry(JSON.parse(await fs.readFile(filePath, "utf8")));
41
+ if (!parsed) {
42
+ await fs.rm(filePath, { force: true });
43
+ continue;
44
+ }
45
+ entries.push(parsed);
46
+ }
47
+ catch {
48
+ await fs.rm(filePath, { force: true }).catch(() => undefined);
49
+ }
50
+ }
51
+ return entries.sort((left, right) => Date.parse(left.created_at) - Date.parse(right.created_at) ||
52
+ left.outbox_id.localeCompare(right.outbox_id));
53
+ }
54
+ export async function summarizeInstallEventOutbox(paths) {
55
+ const entries = await readPendingInstallEventEntries(paths);
56
+ const attempted = entries
57
+ .filter((entry) => entry.last_attempt_at)
58
+ .sort((left, right) => Date.parse(right.last_attempt_at) - Date.parse(left.last_attempt_at))[0];
59
+ return {
60
+ pending_count: entries.length,
61
+ oldest_created_at: entries[0]?.created_at ?? null,
62
+ last_attempt_at: attempted?.last_attempt_at ?? null,
63
+ last_failure_reason: attempted?.last_failure_reason ?? null,
64
+ };
65
+ }
66
+ export async function recordInstallEventAttemptFailure(paths, entry, options) {
67
+ await writeEntry(paths, {
68
+ ...entry,
69
+ last_attempt_at: options.attemptedAt,
70
+ retry_count: entry.retry_count + 1,
71
+ last_failure_reason: options.failureReason,
72
+ });
73
+ }
74
+ export async function removeInstallEventEntry(paths, outboxId) {
75
+ await fs.rm(entryPath(paths, outboxId), { force: true });
76
+ }
77
+ export function installEventOutboxDirectory(paths) {
78
+ return path.join(paths.spool_dir, OUTBOX_DIRECTORY);
79
+ }
80
+ async function pruneInstallEventOutbox(paths) {
81
+ const entries = await readPendingInstallEventEntries(paths);
82
+ const excess = entries.slice(0, Math.max(0, entries.length - MAX_PENDING_ENTRIES));
83
+ await Promise.all(excess.map((entry) => removeInstallEventEntry(paths, entry.outbox_id)));
84
+ }
85
+ async function writeEntry(paths, entry) {
86
+ const directory = installEventOutboxDirectory(paths);
87
+ const filePath = entryPath(paths, entry.outbox_id);
88
+ const tempPath = `${filePath}.tmp-${process.pid}-${crypto.randomUUID()}`;
89
+ await fs.mkdir(directory, { recursive: true, mode: 0o700 });
90
+ if (process.platform !== "win32") {
91
+ await fs.chmod(directory, 0o700).catch(() => undefined);
92
+ }
93
+ try {
94
+ await fs.writeFile(tempPath, `${JSON.stringify(entry, null, 2)}\n`, {
95
+ mode: 0o600,
96
+ flag: "wx",
97
+ });
98
+ await fs.rename(tempPath, filePath);
99
+ if (process.platform !== "win32") {
100
+ await fs.chmod(filePath, 0o600).catch(() => undefined);
101
+ }
102
+ }
103
+ finally {
104
+ await fs.rm(tempPath, { force: true }).catch(() => undefined);
105
+ }
106
+ }
107
+ function entryPath(paths, outboxId) {
108
+ return path.join(installEventOutboxDirectory(paths), `${safeOutboxId(outboxId)}.json`);
109
+ }
110
+ function safeOutboxId(value) {
111
+ return value.replace(/[^a-z0-9_-]/giu, "").slice(0, 120);
112
+ }
113
+ function parseEntry(value) {
114
+ if (!value || typeof value !== "object" || Array.isArray(value))
115
+ return null;
116
+ const record = value;
117
+ const command = installEventCommand(record["command"]);
118
+ const events = Array.isArray(record["events"])
119
+ ? record["events"].map(parseEvent).filter(isPresent)
120
+ : [];
121
+ if (record["schema_version"] !== "cockpit-install-event-outbox.v1" ||
122
+ !stringValue(record["outbox_id"]) ||
123
+ !stringValue(record["created_at"]) ||
124
+ !stringValue(record["dashboard_url"]) ||
125
+ !stringValue(record["cli_version"]) ||
126
+ !stringValue(record["os_platform"]) ||
127
+ !command ||
128
+ events.length === 0) {
129
+ return null;
130
+ }
131
+ return {
132
+ schema_version: "cockpit-install-event-outbox.v1",
133
+ outbox_id: stringValue(record["outbox_id"]),
134
+ created_at: stringValue(record["created_at"]),
135
+ last_attempt_at: stringValue(record["last_attempt_at"]),
136
+ retry_count: finiteNumber(record["retry_count"]),
137
+ last_failure_reason: stringValue(record["last_failure_reason"]),
138
+ dashboard_url: stringValue(record["dashboard_url"]),
139
+ cli_version: stringValue(record["cli_version"]),
140
+ command,
141
+ os_platform: stringValue(record["os_platform"]),
142
+ events: events.slice(0, MAX_EVENTS_PER_ENTRY),
143
+ };
144
+ }
145
+ function parseEvent(value) {
146
+ if (!value || typeof value !== "object" || Array.isArray(value))
147
+ return null;
148
+ const record = value;
149
+ const step = stringValue(record["step"]);
150
+ const status = installEventStatus(record["status"]);
151
+ const at = stringValue(record["at"]);
152
+ if (!step || !status || !at)
153
+ return null;
154
+ return {
155
+ step,
156
+ status,
157
+ ...(stringValue(record["error_code"])
158
+ ? { error_code: stringValue(record["error_code"]) }
159
+ : {}),
160
+ at,
161
+ };
162
+ }
163
+ function installEventCommand(value) {
164
+ return [
165
+ "onboard",
166
+ "update",
167
+ "install",
168
+ "login",
169
+ "sync",
170
+ "backfill",
171
+ "doctor",
172
+ ].includes(String(value))
173
+ ? value
174
+ : null;
175
+ }
176
+ function installEventStatus(value) {
177
+ return value === "ok" || value === "fail" || value === "skipped"
178
+ ? value
179
+ : null;
180
+ }
181
+ function stringValue(value) {
182
+ return typeof value === "string" && value.trim() ? value : null;
183
+ }
184
+ function finiteNumber(value) {
185
+ return typeof value === "number" && Number.isFinite(value) && value >= 0
186
+ ? Math.floor(value)
187
+ : 0;
188
+ }
189
+ function isPresent(value) {
190
+ return value !== null;
191
+ }