@bli-cockpit/cli 0.2.5 → 0.2.8

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
  }
@@ -1,6 +1,7 @@
1
1
  import crypto from "node:crypto";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
+ import { CodexSessionAttributionSchema, } from "@bli-cockpit/telemetry-core";
4
5
  const SPOOL_STATE_FILENAME = "upload-state.json";
5
6
  const MAX_PENDING_UPLOADS = 20;
6
7
  export function emptyUploadSpoolState() {
@@ -11,15 +12,33 @@ export function emptyUploadSpoolState() {
11
12
  last_upload_success_at: null,
12
13
  last_upload_failure_reason: null,
13
14
  pending_uploads: [],
15
+ pending_source_retries: [],
16
+ pending_session_reports: [],
14
17
  };
15
18
  }
16
19
  export async function readLocalUploadSpoolState(paths) {
20
+ const filePath = uploadSpoolStatePath(paths);
21
+ let serialized;
22
+ try {
23
+ serialized = await fs.readFile(filePath, "utf8");
24
+ }
25
+ catch (error) {
26
+ if (isErrnoException(error, "ENOENT"))
27
+ return emptyUploadSpoolState();
28
+ throw uploadSpoolReadError(filePath, "unreadable", error);
29
+ }
30
+ let raw;
31
+ try {
32
+ raw = JSON.parse(serialized);
33
+ }
34
+ catch (error) {
35
+ throw uploadSpoolReadError(filePath, "invalid JSON", error);
36
+ }
17
37
  try {
18
- const raw = JSON.parse(await fs.readFile(uploadSpoolStatePath(paths), "utf8"));
19
38
  return parseUploadSpoolState(raw);
20
39
  }
21
- catch {
22
- return emptyUploadSpoolState();
40
+ catch (error) {
41
+ throw uploadSpoolReadError(filePath, "invalid schema", error);
23
42
  }
24
43
  }
25
44
  export async function summarizeLocalUploadSpool(paths) {
@@ -28,8 +47,14 @@ export async function summarizeLocalUploadSpool(paths) {
28
47
  last_upload_attempt_at: state.last_upload_attempt_at,
29
48
  last_upload_success_at: state.last_upload_success_at,
30
49
  last_upload_failure_reason: state.last_upload_failure_reason,
31
- pending_upload_count: state.pending_uploads.length,
32
- retry_command: state.pending_uploads[0]?.retry_command ?? null,
50
+ pending_upload_count: state.pending_uploads.length +
51
+ state.pending_source_retries.length +
52
+ state.pending_session_reports.length,
53
+ retry_command: state.pending_uploads[0]?.retry_command ??
54
+ (state.pending_source_retries.length > 0 ||
55
+ state.pending_session_reports.length > 0
56
+ ? "cockpit sync"
57
+ : null),
33
58
  };
34
59
  }
35
60
  export async function recordUploadBlocked(paths, options) {
@@ -45,13 +70,19 @@ export async function recordUploadBlocked(paths, options) {
45
70
  }
46
71
  export async function recordUploadSuccess(paths, options) {
47
72
  const state = await readLocalUploadSpoolState(paths);
73
+ const pending = options.clearPendingForContext === false
74
+ ? state.pending_uploads
75
+ : state.pending_uploads.filter((entry) => entry.work_context_id !== options.workContextId);
48
76
  const next = {
49
77
  ...state,
50
78
  updated_at: options.attemptedAt,
51
79
  last_upload_attempt_at: options.attemptedAt,
52
80
  last_upload_success_at: options.attemptedAt,
53
- last_upload_failure_reason: null,
54
- pending_uploads: [],
81
+ last_upload_failure_reason: firstPendingFailureReason({
82
+ ...state,
83
+ pending_uploads: pending,
84
+ }),
85
+ pending_uploads: pending,
55
86
  };
56
87
  await writeUploadSpoolState(paths, next);
57
88
  return next;
@@ -78,69 +109,315 @@ export async function recordUploadFailure(paths, entry) {
78
109
  });
79
110
  return spoolEntry;
80
111
  }
112
+ export async function recordSourceRetryFailure(paths, options) {
113
+ const state = await readLocalUploadSpoolState(paths);
114
+ const retryEntry = {
115
+ source: options.source,
116
+ reason: options.reason,
117
+ last_attempt_at: options.attemptedAt,
118
+ };
119
+ const next = {
120
+ ...state,
121
+ updated_at: options.attemptedAt,
122
+ last_upload_attempt_at: options.attemptedAt,
123
+ last_upload_failure_reason: options.reason,
124
+ pending_source_retries: [
125
+ retryEntry,
126
+ ...state.pending_source_retries.filter((entry) => entry.source !== options.source),
127
+ ],
128
+ };
129
+ await writeUploadSpoolState(paths, next);
130
+ return next;
131
+ }
132
+ export async function clearSourceRetryFailure(paths, source, attemptedAt) {
133
+ const state = await readLocalUploadSpoolState(paths);
134
+ const next = {
135
+ ...state,
136
+ updated_at: attemptedAt,
137
+ pending_source_retries: state.pending_source_retries.filter((entry) => entry.source !== source),
138
+ };
139
+ next.last_upload_failure_reason = firstPendingFailureReason(next);
140
+ await writeUploadSpoolState(paths, next);
141
+ return next;
142
+ }
143
+ export async function recordPendingSessionReport(paths, entry) {
144
+ const state = await readLocalUploadSpoolState(paths);
145
+ const existing = state.pending_session_reports.find((candidate) => candidate.work_context_id === entry.work_context_id);
146
+ const safeIncomingSessions = parseMetadataOnlySessionReportRows(entry.sessions, "pending_session_report.sessions");
147
+ const mergedSessions = mergeSessionReportRows(existing?.sessions ?? [], safeIncomingSessions);
148
+ const pending = {
149
+ report_id: existing?.report_id ?? `session-report-${crypto.randomUUID()}`,
150
+ created_at: existing?.created_at ?? entry.attempted_at,
151
+ last_attempt_at: entry.attempted_at,
152
+ dashboard_url: entry.dashboard_url,
153
+ generated_at: entry.generated_at,
154
+ work_context_id: entry.work_context_id,
155
+ repo_label: entry.repo_label,
156
+ branch: entry.branch,
157
+ repo_fingerprint: entry.repo_fingerprint,
158
+ repo_origin_url: entry.repo_origin_url,
159
+ worktree_label: entry.worktree_label,
160
+ worktree_fingerprint: entry.worktree_fingerprint,
161
+ worktree_is_primary: entry.worktree_is_primary,
162
+ sessions: mergedSessions,
163
+ failure_reason: entry.failure_reason ?? "session_report_pending",
164
+ };
165
+ const next = {
166
+ ...state,
167
+ updated_at: entry.attempted_at,
168
+ last_upload_attempt_at: entry.attempted_at,
169
+ last_upload_failure_reason: pending.failure_reason,
170
+ pending_session_reports: [
171
+ pending,
172
+ ...state.pending_session_reports.filter((candidate) => candidate.report_id !== pending.report_id),
173
+ ],
174
+ };
175
+ await writeUploadSpoolState(paths, next);
176
+ return pending;
177
+ }
178
+ export async function recordSessionReportFailure(paths, options) {
179
+ const state = await readLocalUploadSpoolState(paths);
180
+ const next = {
181
+ ...state,
182
+ updated_at: options.attemptedAt,
183
+ last_upload_attempt_at: options.attemptedAt,
184
+ last_upload_failure_reason: options.reason,
185
+ pending_session_reports: state.pending_session_reports.map((entry) => entry.report_id === options.reportId
186
+ ? {
187
+ ...entry,
188
+ last_attempt_at: options.attemptedAt,
189
+ failure_reason: options.reason,
190
+ }
191
+ : entry),
192
+ };
193
+ await writeUploadSpoolState(paths, next);
194
+ return next;
195
+ }
196
+ export async function recordSessionReportSuccess(paths, options) {
197
+ const state = await readLocalUploadSpoolState(paths);
198
+ const next = {
199
+ ...state,
200
+ updated_at: options.attemptedAt,
201
+ last_upload_attempt_at: options.attemptedAt,
202
+ last_upload_success_at: options.attemptedAt,
203
+ pending_session_reports: state.pending_session_reports.filter((entry) => entry.report_id !== options.reportId),
204
+ };
205
+ next.last_upload_failure_reason = firstPendingFailureReason(next);
206
+ await writeUploadSpoolState(paths, next);
207
+ return next;
208
+ }
81
209
  function parseUploadSpoolState(value) {
82
- if (!value || typeof value !== "object")
83
- return emptyUploadSpoolState();
84
- const record = value;
210
+ const record = requireRecord(value, "root");
211
+ if (record["schema_version"] !== "cockpit-upload-spool.v1") {
212
+ invalidUploadSpoolState("schema_version", 'literal "cockpit-upload-spool.v1"');
213
+ }
85
214
  return {
86
215
  schema_version: "cockpit-upload-spool.v1",
87
- updated_at: optionalString(record["updated_at"]),
88
- last_upload_attempt_at: optionalString(record["last_upload_attempt_at"]),
89
- last_upload_success_at: optionalString(record["last_upload_success_at"]),
90
- last_upload_failure_reason: optionalString(record["last_upload_failure_reason"]),
91
- pending_uploads: Array.isArray(record["pending_uploads"])
92
- ? record["pending_uploads"]
93
- .map(parseUploadSpoolEntry)
94
- .filter((entry) => Boolean(entry))
95
- : [],
96
- };
97
- }
98
- function parseUploadSpoolEntry(value) {
99
- if (!value || typeof value !== "object")
100
- return null;
101
- const record = value;
102
- const spoolId = optionalString(record["spool_id"]);
103
- const createdAt = optionalString(record["created_at"]);
104
- const lastAttemptAt = optionalString(record["last_attempt_at"]);
105
- const dashboardUrl = optionalString(record["dashboard_url"]);
106
- const failureReason = optionalString(record["failure_reason"]);
107
- const retryCommand = optionalString(record["retry_command"]);
108
- if (!spoolId || !createdAt || !lastAttemptAt || !dashboardUrl || !failureReason) {
109
- return null;
110
- }
216
+ updated_at: requireNullableString(record["updated_at"], "updated_at"),
217
+ last_upload_attempt_at: requireNullableString(record["last_upload_attempt_at"], "last_upload_attempt_at"),
218
+ last_upload_success_at: requireNullableString(record["last_upload_success_at"], "last_upload_success_at"),
219
+ last_upload_failure_reason: requireNullableString(record["last_upload_failure_reason"], "last_upload_failure_reason"),
220
+ pending_uploads: requireArray(record["pending_uploads"], "pending_uploads").map((entry, index) => parseUploadSpoolEntry(entry, `pending_uploads[${index}]`)),
221
+ // These fields were added without changing the v1 schema version. Missing
222
+ // fields are therefore a valid legacy state, while present malformed fields
223
+ // must fail closed instead of being silently discarded.
224
+ pending_source_retries: optionalArray(record["pending_source_retries"], "pending_source_retries").map((entry, index) => parseSourceRetryEntry(entry, `pending_source_retries[${index}]`)),
225
+ pending_session_reports: optionalArray(record["pending_session_reports"], "pending_session_reports").map((entry, index) => parsePendingSessionReport(entry, `pending_session_reports[${index}]`)),
226
+ };
227
+ }
228
+ function parseUploadSpoolEntry(value, fieldPath) {
229
+ const record = requireRecord(value, fieldPath);
230
+ const retryCommand = record["retry_command"] === undefined
231
+ ? "cockpit sync"
232
+ : requireString(record["retry_command"], `${fieldPath}.retry_command`);
111
233
  return {
112
- spool_id: spoolId,
113
- created_at: createdAt,
114
- last_attempt_at: lastAttemptAt,
115
- dashboard_url: dashboardUrl,
116
- work_context_id: optionalString(record["work_context_id"]),
117
- ticket_id: optionalString(record["ticket_id"]),
118
- repo_label: optionalString(record["repo_label"]),
119
- branch: optionalString(record["branch"]),
120
- event_count: optionalNumber(record["event_count"]),
121
- source_scan_count: optionalNumber(record["source_scan_count"]),
122
- risk_flag_count: optionalNumber(record["risk_flag_count"]),
123
- raw_evidence_file_count: optionalNumber(record["raw_evidence_file_count"]),
124
- failure_reason: failureReason,
125
- retry_command: retryCommand ?? "cockpit sync",
234
+ spool_id: requireString(record["spool_id"], `${fieldPath}.spool_id`),
235
+ created_at: requireString(record["created_at"], `${fieldPath}.created_at`),
236
+ last_attempt_at: requireString(record["last_attempt_at"], `${fieldPath}.last_attempt_at`),
237
+ dashboard_url: requireString(record["dashboard_url"], `${fieldPath}.dashboard_url`),
238
+ work_context_id: requireNullableString(record["work_context_id"], `${fieldPath}.work_context_id`),
239
+ ticket_id: requireNullableString(record["ticket_id"], `${fieldPath}.ticket_id`),
240
+ repo_label: requireNullableString(record["repo_label"], `${fieldPath}.repo_label`),
241
+ branch: requireNullableString(record["branch"], `${fieldPath}.branch`),
242
+ event_count: requireNonNegativeInteger(record["event_count"], `${fieldPath}.event_count`),
243
+ source_scan_count: requireNonNegativeInteger(record["source_scan_count"], `${fieldPath}.source_scan_count`),
244
+ risk_flag_count: requireNonNegativeInteger(record["risk_flag_count"], `${fieldPath}.risk_flag_count`),
245
+ raw_evidence_file_count: requireNonNegativeInteger(record["raw_evidence_file_count"], `${fieldPath}.raw_evidence_file_count`),
246
+ retry_sources: parseRetrySources(record["retry_sources"], `${fieldPath}.retry_sources`),
247
+ failure_reason: requireString(record["failure_reason"], `${fieldPath}.failure_reason`),
248
+ retry_command: retryCommand,
126
249
  };
127
250
  }
128
251
  async function writeUploadSpoolState(paths, state) {
129
252
  const filePath = uploadSpoolStatePath(paths);
130
- await fs.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
131
- await fs.writeFile(filePath, `${JSON.stringify(state, null, 2)}\n`, {
132
- mode: 0o600,
133
- });
134
- if (process.platform !== "win32") {
135
- await fs.chmod(filePath, 0o600).catch(() => undefined);
253
+ const directoryPath = path.dirname(filePath);
254
+ const serialized = serializeUploadSpoolState(state);
255
+ const tempPath = path.join(directoryPath, `.${path.basename(filePath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
256
+ await fs.mkdir(directoryPath, { recursive: true, mode: 0o700 });
257
+ let handle = null;
258
+ let tempCreated = false;
259
+ let renamed = false;
260
+ try {
261
+ handle = await fs.open(tempPath, "wx", 0o600);
262
+ tempCreated = true;
263
+ if (process.platform !== "win32") {
264
+ await handle.chmod(0o600);
265
+ }
266
+ await handle.writeFile(serialized, "utf8");
267
+ await handle.sync();
268
+ await handle.close();
269
+ handle = null;
270
+ await fs.rename(tempPath, filePath);
271
+ renamed = true;
272
+ await fsyncDirectoryBestEffort(directoryPath);
136
273
  }
274
+ catch (error) {
275
+ await handle?.close().catch(() => undefined);
276
+ if (tempCreated && !renamed) {
277
+ await fs.unlink(tempPath).catch(() => undefined);
278
+ }
279
+ throw error;
280
+ }
281
+ }
282
+ function serializeUploadSpoolState(state) {
283
+ const json = JSON.stringify(state);
284
+ const canonical = parseUploadSpoolState(JSON.parse(json));
285
+ return `${JSON.stringify(canonical, null, 2)}\n`;
137
286
  }
138
287
  function uploadSpoolStatePath(paths) {
139
288
  return path.join(paths.spool_dir, SPOOL_STATE_FILENAME);
140
289
  }
141
- function optionalString(value) {
142
- return typeof value === "string" && value.trim() ? value : null;
290
+ function parseRetrySources(value, fieldPath) {
291
+ if (value === undefined)
292
+ return [];
293
+ const values = requireArray(value, fieldPath);
294
+ return [...new Set(values.map((source) => parseRetrySource(source, fieldPath)))];
295
+ }
296
+ function parseSourceRetryEntry(value, fieldPath) {
297
+ const record = requireRecord(value, fieldPath);
298
+ return {
299
+ source: parseRetrySource(record["source"], `${fieldPath}.source`),
300
+ reason: requireString(record["reason"], `${fieldPath}.reason`),
301
+ last_attempt_at: requireString(record["last_attempt_at"], `${fieldPath}.last_attempt_at`),
302
+ };
303
+ }
304
+ function parsePendingSessionReport(value, fieldPath) {
305
+ const record = requireRecord(value, fieldPath);
306
+ const sessions = parseMetadataOnlySessionReportRows(record["sessions"], `${fieldPath}.sessions`);
307
+ return {
308
+ report_id: requireString(record["report_id"], `${fieldPath}.report_id`),
309
+ created_at: requireString(record["created_at"], `${fieldPath}.created_at`),
310
+ last_attempt_at: requireString(record["last_attempt_at"], `${fieldPath}.last_attempt_at`),
311
+ dashboard_url: requireString(record["dashboard_url"], `${fieldPath}.dashboard_url`),
312
+ generated_at: requireString(record["generated_at"], `${fieldPath}.generated_at`),
313
+ work_context_id: requireString(record["work_context_id"], `${fieldPath}.work_context_id`),
314
+ repo_label: requireString(record["repo_label"], `${fieldPath}.repo_label`),
315
+ branch: requireString(record["branch"], `${fieldPath}.branch`),
316
+ repo_fingerprint: requireString(record["repo_fingerprint"], `${fieldPath}.repo_fingerprint`),
317
+ repo_origin_url: record["repo_origin_url"] === undefined
318
+ ? null
319
+ : requireNullableString(record["repo_origin_url"], `${fieldPath}.repo_origin_url`),
320
+ worktree_label: requireString(record["worktree_label"], `${fieldPath}.worktree_label`),
321
+ worktree_fingerprint: requireString(record["worktree_fingerprint"], `${fieldPath}.worktree_fingerprint`),
322
+ worktree_is_primary: requireBoolean(record["worktree_is_primary"], `${fieldPath}.worktree_is_primary`),
323
+ sessions,
324
+ failure_reason: requireString(record["failure_reason"], `${fieldPath}.failure_reason`),
325
+ };
143
326
  }
144
- function optionalNumber(value) {
145
- return typeof value === "number" && Number.isFinite(value) ? value : 0;
327
+ function parseMetadataOnlySessionReportRows(value, fieldPath) {
328
+ const sessions = requireArray(value, fieldPath).map((session, index) => {
329
+ const parsed = CodexSessionAttributionSchema.safeParse(session);
330
+ if (!parsed.success) {
331
+ invalidUploadSpoolState(`${fieldPath}[${index}]`, "metadata-only Codex session attribution");
332
+ }
333
+ return parsed.data;
334
+ });
335
+ if (sessions.length === 0) {
336
+ invalidUploadSpoolState(fieldPath, "non-empty array");
337
+ }
338
+ return sessions;
339
+ }
340
+ function parseRetrySource(value, fieldPath) {
341
+ if (value === "codex" || value === "claude_code")
342
+ return value;
343
+ return invalidUploadSpoolState(fieldPath, '"codex" or "claude_code"');
344
+ }
345
+ function mergeSessionReportRows(existing, incoming) {
346
+ const bySession = new Map();
347
+ for (const session of [...existing, ...incoming]) {
348
+ bySession.set(`${session.source ?? "codex"}:${session.codex_session_id}`, session);
349
+ }
350
+ return [...bySession.values()];
351
+ }
352
+ function firstPendingFailureReason(state) {
353
+ return (state.pending_uploads[0]?.failure_reason ??
354
+ state.pending_source_retries[0]?.reason ??
355
+ state.pending_session_reports[0]?.failure_reason ??
356
+ null);
357
+ }
358
+ function requireRecord(value, fieldPath) {
359
+ if (value && typeof value === "object" && !Array.isArray(value)) {
360
+ return value;
361
+ }
362
+ return invalidUploadSpoolState(fieldPath, "object");
363
+ }
364
+ function requireArray(value, fieldPath) {
365
+ if (Array.isArray(value))
366
+ return value;
367
+ return invalidUploadSpoolState(fieldPath, "array");
368
+ }
369
+ function optionalArray(value, fieldPath) {
370
+ return value === undefined ? [] : requireArray(value, fieldPath);
371
+ }
372
+ function requireString(value, fieldPath) {
373
+ if (typeof value === "string" && value.trim())
374
+ return value;
375
+ return invalidUploadSpoolState(fieldPath, "non-empty string");
376
+ }
377
+ function requireNullableString(value, fieldPath) {
378
+ if (value === null)
379
+ return null;
380
+ return requireString(value, fieldPath);
381
+ }
382
+ function requireNonNegativeInteger(value, fieldPath) {
383
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0) {
384
+ return value;
385
+ }
386
+ return invalidUploadSpoolState(fieldPath, "non-negative integer");
387
+ }
388
+ function requireBoolean(value, fieldPath) {
389
+ if (typeof value === "boolean")
390
+ return value;
391
+ return invalidUploadSpoolState(fieldPath, "boolean");
392
+ }
393
+ function invalidUploadSpoolState(fieldPath, expected) {
394
+ throw new Error(`Upload spool state schema mismatch at ${fieldPath}; expected ${expected}.`);
395
+ }
396
+ function isErrnoException(error, code) {
397
+ return (error instanceof Error &&
398
+ "code" in error &&
399
+ error.code === code);
400
+ }
401
+ function uploadSpoolReadError(filePath, classification, cause) {
402
+ const detail = cause &&
403
+ typeof cause === "object" &&
404
+ "code" in cause &&
405
+ typeof cause.code === "string"
406
+ ? ` (${cause.code})`
407
+ : "";
408
+ return new Error(`Upload spool state at ${filePath} is ${classification}; refusing to treat pending delivery state as empty.${detail}`);
409
+ }
410
+ async function fsyncDirectoryBestEffort(directoryPath) {
411
+ let handle = null;
412
+ try {
413
+ handle = await fs.open(directoryPath, "r");
414
+ await handle.sync();
415
+ }
416
+ catch {
417
+ // Directory fsync is not supported by every host/filesystem (notably some
418
+ // Windows versions). The file itself was fsynced before the atomic rename.
419
+ }
420
+ finally {
421
+ await handle?.close().catch(() => undefined);
422
+ }
146
423
  }