@minhspark/codex-mcp-bridge 1.22.13 → 1.22.14

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.
@@ -0,0 +1,129 @@
1
+ import fs from "node:fs";
2
+ import net from "node:net";
3
+ import path from "node:path";
4
+
5
+ import { codexHome, resolveRelayThreadId } from "./native-relay.mjs";
6
+ import { isCodexDesktopRunning, openThreadInCodexApp } from "./platform.mjs";
7
+ import { captureProjectScope } from "./project-scope.mjs";
8
+ import { readRolloutFirstRecord } from "./rollout-reader.mjs";
9
+
10
+ const THREAD_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
11
+ const ROLLOUT = /^rollout-.+-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl(?:\.zst)?$/i;
12
+ export const RELAY_WAKE_WAIT_MS = 20000;
13
+ export const RELAY_WAKE_COOLDOWN_MS = 60000;
14
+ const MAX_DAY_DIRECTORIES = 14;
15
+ const MAX_CANDIDATES = 40;
16
+
17
+ export function relayWakeEnabled(env = process.env, platform = process.platform) {
18
+ const value = (env.CODEX_BRIDGE_RELAY_WAKE ?? "").toLowerCase();
19
+ if (["0", "off", "false"].includes(value)) return false;
20
+ return platform === "darwin" || platform === "win32";
21
+ }
22
+
23
+ function dayDirectories(sessions) {
24
+ const directories = [];
25
+ const children = (directory, pattern) => {
26
+ try { return fs.readdirSync(directory, { withFileTypes: true }).filter((entry) => entry.isDirectory() && pattern.test(entry.name)).map((entry) => entry.name).sort().reverse(); }
27
+ catch { return []; }
28
+ };
29
+ for (const year of children(sessions, /^\d{4}$/)) {
30
+ for (const month of children(path.join(sessions, year), /^\d{2}$/)) {
31
+ for (const day of children(path.join(sessions, year, month), /^\d{2}$/)) {
32
+ directories.push(path.join(sessions, year, month, day));
33
+ if (directories.length >= MAX_DAY_DIRECTORIES) return directories;
34
+ }
35
+ }
36
+ }
37
+ return directories;
38
+ }
39
+
40
+ function sameProject(senderCwd, taskCwd) {
41
+ try { captureProjectScope(senderCwd, taskCwd); return true; }
42
+ catch { return false; }
43
+ }
44
+
45
+ export async function recentDesktopTaskFor(cwd, { env = process.env, sessions = path.join(codexHome(env), "sessions"), exclude = [] } = {}) {
46
+ if (typeof cwd !== "string" || !cwd) return null;
47
+ const excluded = new Set(exclude.filter(Boolean));
48
+ const candidates = [];
49
+ for (const directory of dayDirectories(sessions)) {
50
+ let entries;
51
+ try { entries = fs.readdirSync(directory, { withFileTypes: true }); } catch { continue; }
52
+ for (const entry of entries) {
53
+ const match = entry.isFile() ? ROLLOUT.exec(entry.name) : null;
54
+ if (!match || excluded.has(match[1])) continue;
55
+ const file = path.join(directory, entry.name);
56
+ try { candidates.push({ file, threadId: match[1], mtimeMs: fs.lstatSync(file).mtimeMs }); } catch {}
57
+ }
58
+ if (candidates.length >= MAX_CANDIDATES) break;
59
+ }
60
+ candidates.sort((left, right) => right.mtimeMs - left.mtimeMs);
61
+ for (const candidate of candidates.slice(0, MAX_CANDIDATES)) {
62
+ let record;
63
+ try { record = await readRolloutFirstRecord(candidate.file, { maxBytes: 256 * 1024 }); } catch { continue; }
64
+ const meta = record?.type === "session_meta" ? record.payload : null;
65
+ if (meta?.id !== candidate.threadId || meta.originator !== "Codex Desktop" || typeof meta.cwd !== "string") continue;
66
+ if (sameProject(cwd, meta.cwd)) return candidate.threadId;
67
+ }
68
+ return null;
69
+ }
70
+
71
+ export function endpointAnswers(socketPath, { timeoutMs = 1000 } = {}) {
72
+ if (typeof socketPath !== "string" || !socketPath) return Promise.resolve(false);
73
+ return new Promise((resolve) => {
74
+ const probe = net.connect({ path: socketPath });
75
+ const done = (answer) => {
76
+ globalThis.clearTimeout(timer);
77
+ probe.destroy();
78
+ resolve(answer);
79
+ };
80
+ const timer = globalThis.setTimeout(() => done(false), timeoutMs);
81
+ probe.once("connect", () => done(true));
82
+ probe.once("error", () => done(false));
83
+ });
84
+ }
85
+
86
+ export function createRelayWake({
87
+ env = process.env,
88
+ enabled = relayWakeEnabled(env),
89
+ desktopRunning = () => isCodexDesktopRunning(),
90
+ senderCwd = () => null,
91
+ findTask = (cwd, exclude) => recentDesktopTaskFor(cwd, { env, exclude }),
92
+ executorThreadId = () => { try { return resolveRelayThreadId(env).threadId; } catch { return null; } },
93
+ open = (threadId) => openThreadInCodexApp(threadId, { activate: false }),
94
+ answers = endpointAnswers,
95
+ waitMs = RELAY_WAKE_WAIT_MS,
96
+ cooldownMs = RELAY_WAKE_COOLDOWN_MS,
97
+ pollMs = 250,
98
+ now = Date.now,
99
+ sleep = (ms) => new Promise((resolve) => globalThis.setTimeout(resolve, ms)),
100
+ log = () => {},
101
+ } = {}) {
102
+ let inFlight = null;
103
+ let failedAt = -Infinity;
104
+ const attempt = async ({ socketPath, threadId, deadline }) => {
105
+ if (!enabled || now() - failedAt < cooldownMs || !await desktopRunning()) return null;
106
+ const executor = executorThreadId();
107
+ const target = typeof threadId === "string" && THREAD_ID.test(threadId) && threadId !== executor
108
+ ? threadId : await findTask(senderCwd(), [executor]);
109
+ if (!target) return null;
110
+ try { await open(target); }
111
+ catch (error) {
112
+ failedAt = now();
113
+ log(`could not open Codex task ${target} to start the native relay: ${error.message}`);
114
+ return null;
115
+ }
116
+ const until = Math.min(now() + waitMs, deadline ?? Infinity);
117
+ while (now() < until) {
118
+ if (await answers(socketPath)) {
119
+ log(`started the Codex Desktop native relay by opening task ${target} in the background`);
120
+ return { threadId: target };
121
+ }
122
+ await sleep(pollMs);
123
+ }
124
+ failedAt = now();
125
+ log(`opened Codex task ${target}, but the native relay did not start within ${waitMs} ms`);
126
+ return null;
127
+ };
128
+ return (request) => inFlight ??= attempt(request).finally(() => { inFlight = null; });
129
+ }
@@ -1,8 +1,10 @@
1
+ import { execFile, execFileSync } from "node:child_process";
1
2
  import { createHash, randomUUID } from "node:crypto";
2
3
  import fs from "node:fs";
3
4
  import os from "node:os";
4
5
  import path from "node:path";
5
6
  import { createRequire } from "node:module";
7
+ import { promisify } from "node:util";
6
8
 
7
9
  function digestFiles(root, files) {
8
10
  const hash = createHash("sha256");
@@ -239,7 +241,309 @@ function removeOwned(directory, cache) {
239
241
  fs.rmSync(directory, { recursive: true, force: true });
240
242
  }
241
243
 
242
- export function createReleaseSnapshot(root, { cache = snapshotRoot(), expectedRevision = sourceRevision(root) } = {}) {
244
+ const LEASES = ".leases";
245
+ const LEASE_PATTERN = /^([a-f0-9]{64})\.(\d+)\.[0-9a-f-]{36}\.lease$/;
246
+ const OWNED_TEMPORARY_PATTERN = /^\.(?:preparing|retired|linking)-(\d+)-[0-9a-f-]{36}$/;
247
+ const DIGEST_TEMPORARY_PATTERN = /^\.(?:digests|prune-lock)-(\d+)-[0-9a-f-]{36}\.tmp$/;
248
+ const DIGEST_FILE_PATTERN = /^\.digests-[a-f0-9]{64}\.json$/;
249
+ const DIGEST_HEADER = /^\{"schema":1,"directory":("(?:[^"\\]|\\.)*")/;
250
+ const PRUNE_LOCK = ".prune.lock";
251
+ const PRUNE_STAMP = ".prune-stamp";
252
+ const LEASE_HEARTBEAT_MS = 5 * 60 * 1000;
253
+ const LEASE_FRESH_MS = 3 * LEASE_HEARTBEAT_MS;
254
+ const LEASE_OWNER_MAX_MS = 7 * 24 * 60 * 60 * 1000;
255
+ const PRUNE_LOCK_STALE_MS = 10 * 60 * 1000;
256
+ const TEMPORARY_GRACE_MS = 60 * 60 * 1000;
257
+ const CLONE_TIMEOUT_MS = 120000;
258
+ const PROCESS_LIST_TIMEOUT_MS = 60000;
259
+ const MAX_DIGEST_HEADERS = 256;
260
+ const RUNTIME_RETENTION = Object.freeze({ retain: 3, graceMs: 24 * 60 * 60 * 1000, minIntervalMs: 60 * 60 * 1000, maxRemovals: 8 });
261
+
262
+ const execFileAsync = promisify(execFile);
263
+ const activeLeases = new Set();
264
+
265
+ function processAlive(pid) {
266
+ if (!Number.isSafeInteger(pid) || pid <= 0) return false;
267
+ if (pid === process.pid) return true;
268
+ try { process.kill(pid, 0); return true; }
269
+ catch (error) { return error.code === "EPERM"; }
270
+ }
271
+
272
+ function touch(target, now = new Date()) {
273
+ try { fs.utimesSync(target, now, now); return true; } catch { return false; }
274
+ }
275
+
276
+ function isDirectory(target) {
277
+ try { const info = fs.lstatSync(target); return info.isDirectory() && !info.isSymbolicLink(); } catch { return false; }
278
+ }
279
+
280
+ function removeIfFile(file) {
281
+ try {
282
+ const info = fs.lstatSync(file);
283
+ if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1) return false;
284
+ fs.rmSync(file, { force: true });
285
+ return true;
286
+ } catch { return false; }
287
+ }
288
+
289
+ function releaseActiveLeases() {
290
+ for (const lease of activeLeases) lease.release();
291
+ }
292
+
293
+ function leaseSnapshot(cache, key, target) {
294
+ const directory = path.join(cache, LEASES);
295
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
296
+ if (!isDirectory(directory)) throw new Error("The runtime lease directory is not a directory");
297
+ const file = path.join(directory, `${key}.${process.pid}.${randomUUID()}.lease`);
298
+ const write = () => fs.writeFileSync(file, `${JSON.stringify({ schema: 1, key, pid: process.pid })}\n`, { flag: "wx", mode: 0o600 });
299
+ write();
300
+ if (!activeLeases.size) process.once("exit", releaseActiveLeases);
301
+ const timer = setInterval(() => {
302
+ if (touch(file)) return;
303
+ try { write(); } catch {}
304
+ }, LEASE_HEARTBEAT_MS);
305
+ timer.unref?.();
306
+ const lease = {
307
+ file,
308
+ released: false,
309
+ release() {
310
+ if (lease.released) return;
311
+ lease.released = true;
312
+ clearInterval(timer);
313
+ activeLeases.delete(lease);
314
+ if (!activeLeases.size) process.removeListener("exit", releaseActiveLeases);
315
+ try { fs.rmSync(file, { force: true }); } catch {}
316
+ touch(target);
317
+ },
318
+ };
319
+ activeLeases.add(lease);
320
+ return lease;
321
+ }
322
+
323
+ function readLeases(cache, now) {
324
+ const directory = path.join(cache, LEASES);
325
+ if (!isDirectory(directory)) return [];
326
+ const leases = [];
327
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
328
+ const match = LEASE_PATTERN.exec(entry.name);
329
+ if (!match || !entry.isFile()) continue;
330
+ const file = path.join(directory, entry.name);
331
+ let info;
332
+ try { info = fs.lstatSync(file); } catch { continue; }
333
+ const age = now - info.mtimeMs;
334
+ const pid = Number(match[2]);
335
+ leases.push({ file, key: match[1], usedAt: info.mtimeMs, live: age < LEASE_FRESH_MS || (age < LEASE_OWNER_MAX_MS && processAlive(pid)) });
336
+ }
337
+ return leases;
338
+ }
339
+
340
+ export async function runningCommandLines() {
341
+ const options = { timeout: PROCESS_LIST_TIMEOUT_MS, maxBuffer: 64 * 1024 * 1024, windowsHide: true };
342
+ let stdout;
343
+ if (process.platform === "win32") {
344
+ const shell = path.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
345
+ ({ stdout } = await execFileAsync(shell, ["-NoProfile", "-NonInteractive", "-Command", "Get-CimInstance Win32_Process | ForEach-Object { $_.CommandLine }"], options));
346
+ } else {
347
+ const ps = ["/bin/ps", "/usr/bin/ps"].find((candidate) => fs.existsSync(candidate));
348
+ if (!ps) throw new Error("No process listing tool is available");
349
+ ({ stdout } = await execFileAsync(ps, ["-A", "-ww", "-o", "args="], { ...options, env: { ...process.env, LC_ALL: "C" } }));
350
+ }
351
+ const lines = stdout.split(/\r?\n/).filter(Boolean);
352
+ if (!lines.length) throw new Error("The process listing is empty");
353
+ return lines;
354
+ }
355
+
356
+ function createPruneLock(cache, lock) {
357
+ const staged = path.join(cache, `.prune-lock-${process.pid}-${randomUUID()}.tmp`);
358
+ fs.writeFileSync(staged, `${process.pid}\n`, { flag: "wx", mode: 0o600 });
359
+ try { fs.linkSync(staged, lock); }
360
+ catch (error) {
361
+ if (error.code === "EEXIST") throw error;
362
+ fs.writeFileSync(lock, `${process.pid}\n`, { flag: "wx", mode: 0o600 });
363
+ } finally { fs.rmSync(staged, { force: true }); }
364
+ }
365
+
366
+ function takeOverStalePruneLock(cache, lock, now) {
367
+ const info = fs.lstatSync(lock, { bigint: true });
368
+ if (!info.isFile() || info.isSymbolicLink()) return false;
369
+ const text = fs.readFileSync(lock, "utf8").trim();
370
+ const owner = /^\d+$/.test(text) ? Number(text) : null;
371
+ if (now - Number(info.mtimeMs) < PRUNE_LOCK_STALE_MS && (owner === null || processAlive(owner))) return false;
372
+ const moved = path.join(cache, `.prune-lock-${process.pid}-${randomUUID()}.tmp`);
373
+ fs.renameSync(lock, moved);
374
+ let reclaimed = false;
375
+ try {
376
+ const descriptor = fs.openSync(moved, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0));
377
+ try {
378
+ const examined = fs.fstatSync(descriptor, { bigint: true });
379
+ reclaimed = examined.isFile() && examined.dev === info.dev && examined.ino === info.ino && examined.mtimeNs === info.mtimeNs && examined.size === info.size
380
+ && fs.readFileSync(descriptor, "utf8").trim() === text;
381
+ } finally { fs.closeSync(descriptor); }
382
+ } catch { reclaimed = false; }
383
+ if (!reclaimed) { try { fs.linkSync(moved, lock); } catch {} }
384
+ fs.rmSync(moved, { force: true });
385
+ return reclaimed;
386
+ }
387
+
388
+ function acquirePruneLock(cache, now) {
389
+ const lock = path.join(cache, PRUNE_LOCK);
390
+ for (let attempt = 0; attempt < 2; attempt++) {
391
+ try {
392
+ createPruneLock(cache, lock);
393
+ return () => {
394
+ try { if (fs.readFileSync(lock, "utf8").trim() === String(process.pid)) fs.rmSync(lock, { force: true }); } catch {}
395
+ };
396
+ } catch (error) {
397
+ if (error.code !== "EEXIST" || attempt) return null;
398
+ try { if (!takeOverStalePruneLock(cache, lock, now)) return null; } catch { return null; }
399
+ }
400
+ }
401
+ return null;
402
+ }
403
+
404
+ function claimPruneInterval(stamp, now, minIntervalMs) {
405
+ let info = null;
406
+ try { info = fs.lstatSync(stamp); } catch (error) { if (error.code !== "ENOENT") return "The prune stamp could not be read"; }
407
+ if (info && (!info.isFile() || info.isSymbolicLink())) return "The prune stamp is not a regular file";
408
+ if (info && now - info.mtimeMs < minIntervalMs) return "The runtime cache was pruned recently";
409
+ let descriptor;
410
+ try {
411
+ descriptor = fs.openSync(stamp, fs.constants.O_WRONLY | fs.constants.O_CREAT | (fs.constants.O_NOFOLLOW ?? 0), 0o600);
412
+ if (!fs.fstatSync(descriptor).isFile()) return "The prune stamp is not a regular file";
413
+ const time = new Date(now);
414
+ fs.futimesSync(descriptor, time, time);
415
+ return null;
416
+ } catch { return "The prune stamp could not be updated"; }
417
+ finally { if (descriptor !== undefined) fs.closeSync(descriptor); }
418
+ }
419
+
420
+ function digestDirectory(file) {
421
+ let descriptor;
422
+ try {
423
+ descriptor = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
424
+ const buffer = Buffer.alloc(8192);
425
+ const count = fs.readSync(descriptor, buffer, 0, buffer.length, 0);
426
+ const match = DIGEST_HEADER.exec(buffer.subarray(0, count).toString("utf8"));
427
+ return match ? JSON.parse(match[1]) : null;
428
+ } catch { return null; }
429
+ finally { if (descriptor !== undefined) fs.closeSync(descriptor); }
430
+ }
431
+
432
+ function sweepCacheFiles(cache, entries, now, report) {
433
+ let headers = 0;
434
+ for (const entry of entries) {
435
+ const target = path.join(cache, entry.name);
436
+ const temporary = OWNED_TEMPORARY_PATTERN.exec(entry.name) ?? DIGEST_TEMPORARY_PATTERN.exec(entry.name);
437
+ if (temporary) {
438
+ let info;
439
+ try { info = fs.lstatSync(target); } catch { continue; }
440
+ if (info.isSymbolicLink() || now - info.mtimeMs < TEMPORARY_GRACE_MS || processAlive(Number(temporary[1]))) continue;
441
+ if (info.isDirectory()) { try { removeOwned(target, cache); report.temporaries++; } catch {} }
442
+ else if (removeIfFile(target)) report.temporaries++;
443
+ continue;
444
+ }
445
+ if (!DIGEST_FILE_PATTERN.test(entry.name) || !entry.isFile() || headers >= MAX_DIGEST_HEADERS) continue;
446
+ headers++;
447
+ const directory = digestDirectory(target);
448
+ if (typeof directory === "string" && !isDirectory(directory) && removeIfFile(target)) report.digests++;
449
+ }
450
+ }
451
+
452
+ async function retireSnapshot(cache, key, usedAt) {
453
+ const target = path.join(cache, key);
454
+ const leased = () => readLeases(cache, Date.now()).some((lease) => lease.key === key && lease.live);
455
+ let info;
456
+ try { info = fs.lstatSync(target); } catch { return "kept"; }
457
+ if (!info.isDirectory() || info.isSymbolicLink() || info.mtimeMs > usedAt || leased()) return "kept";
458
+ const retired = path.join(cache, `.retired-${process.pid}-${randomUUID()}`);
459
+ try { fs.renameSync(target, retired); } catch { return "kept"; }
460
+ if (leased()) {
461
+ try { fs.renameSync(retired, target); }
462
+ catch { if (isDirectory(target) && isDirectory(retired)) removeOwned(retired, cache); }
463
+ return "kept";
464
+ }
465
+ if (!isDirectory(retired)) return "kept";
466
+ removeIfFile(path.join(cache, `.digests-${createHash("sha256").update(target).digest("hex")}.json`));
467
+ try { await fs.promises.rm(retired, { recursive: true, force: true }); } catch { return "incomplete"; }
468
+ return isDirectory(retired) ? "incomplete" : "removed";
469
+ }
470
+
471
+ function pruneCandidates(cache, entries, leases, { keep, retain, graceMs }, now) {
472
+ const live = new Set(leases.filter((lease) => lease.live).map((lease) => lease.key));
473
+ const snapshots = [];
474
+ for (const entry of entries) {
475
+ if (!DIGEST_PATTERN.test(entry.name) || !entry.isDirectory()) continue;
476
+ let info;
477
+ try { info = fs.lstatSync(path.join(cache, entry.name)); } catch { continue; }
478
+ if (!info.isDirectory() || info.isSymbolicLink()) continue;
479
+ const usedAt = Math.max(info.mtimeMs, ...leases.filter((lease) => lease.key === entry.name).map((lease) => lease.usedAt));
480
+ snapshots.push({ key: entry.name, usedAt });
481
+ }
482
+ snapshots.sort((left, right) => right.usedAt - left.usedAt || left.key.localeCompare(right.key, "en"));
483
+ const protectedKeys = new Set(keep);
484
+ const candidates = snapshots.filter((snapshot, index) => index >= retain && !protectedKeys.has(snapshot.key)
485
+ && !live.has(snapshot.key) && now - snapshot.usedAt >= graceMs).reverse();
486
+ return { snapshots, candidates };
487
+ }
488
+
489
+ export async function pruneReleaseSnapshots({ cache = snapshotRoot(), keep = [], retain = RUNTIME_RETENTION.retain, graceMs = RUNTIME_RETENTION.graceMs,
490
+ minIntervalMs = RUNTIME_RETENTION.minIntervalMs, maxRemovals = RUNTIME_RETENTION.maxRemovals, listProcesses = runningCommandLines } = {}) {
491
+ const report = { removed: [], kept: [], incomplete: [], leases: 0, temporaries: 0, digests: 0, skipped: null };
492
+ if (!isDirectory(cache)) { report.skipped = "The runtime cache is not a directory"; return report; }
493
+ cache = fs.realpathSync.native(cache);
494
+ const now = Date.now();
495
+ const unlock = acquirePruneLock(cache, now);
496
+ if (!unlock) { report.skipped = "Another prune is running"; return report; }
497
+ try {
498
+ const stamp = path.join(cache, PRUNE_STAMP);
499
+ report.skipped = claimPruneInterval(stamp, now, minIntervalMs);
500
+ if (report.skipped) return report;
501
+ const leases = readLeases(cache, now);
502
+ for (const lease of leases) if (!lease.live && removeIfFile(lease.file)) report.leases++;
503
+ const entries = fs.readdirSync(cache, { withFileTypes: true });
504
+ sweepCacheFiles(cache, entries, now, report);
505
+ const { snapshots, candidates } = pruneCandidates(cache, entries, leases, { keep, retain, graceMs }, now);
506
+ const bounded = candidates.slice(0, Math.max(0, maxRemovals));
507
+ report.kept = snapshots.filter((snapshot) => !bounded.includes(snapshot)).map((snapshot) => snapshot.key);
508
+ if (!bounded.length) return report;
509
+ let commandLines;
510
+ try { commandLines = (await listProcesses()).map((line) => String(line).toLowerCase()); }
511
+ catch (error) {
512
+ report.kept.push(...bounded.map((snapshot) => snapshot.key));
513
+ report.skipped = `Running processes could not be listed: ${error.message}`;
514
+ return report;
515
+ }
516
+ for (const { key, usedAt } of bounded) {
517
+ const outcome = commandLines.some((line) => line.includes(key)) ? "kept" : await retireSnapshot(cache, key, usedAt);
518
+ report[outcome].push(key);
519
+ }
520
+ return report;
521
+ } finally { unlock(); }
522
+ }
523
+
524
+ const cloneSupported = () => process.platform === "darwin" && fs.existsSync("/bin/cp");
525
+
526
+ function cloneRelease(entries, temporary, cache) {
527
+ const farm = path.join(cache, `.linking-${process.pid}-${randomUUID()}`);
528
+ try {
529
+ fs.mkdirSync(farm, { mode: 0o700 });
530
+ const created = new Set();
531
+ for (const [file, source] of entries) {
532
+ const link = path.join(farm, file);
533
+ const parent = path.dirname(link);
534
+ if (!created.has(parent)) { fs.mkdirSync(parent, { recursive: true, mode: 0o700 }); created.add(parent); }
535
+ fs.symlinkSync(source, link);
536
+ }
537
+ execFileSync("/bin/cp", ["-c", "-R", "-L", "--", `${farm}${path.sep}`, temporary], { stdio: "ignore", timeout: CLONE_TIMEOUT_MS });
538
+ return true;
539
+ } catch {
540
+ return false;
541
+ } finally {
542
+ try { removeOwned(farm, cache); } catch {}
543
+ }
544
+ }
545
+
546
+ export function createReleaseSnapshot(root, { cache = snapshotRoot(), expectedRevision = sourceRevision(root), lease = false } = {}) {
243
547
  root = fs.realpathSync.native(root);
244
548
  if (fs.existsSync(cache) && fs.lstatSync(cache).isSymbolicLink()) throw new Error("The runtime cache cannot be a symbolic link");
245
549
  const missing = [];
@@ -263,43 +567,66 @@ export function createReleaseSnapshot(root, { cache = snapshotRoot(), expectedRe
263
567
  if (fs.lstatSync(directory).isSymbolicLink()) throw new Error("The release directory cannot be a symbolic link");
264
568
  return sourceRevision(directory) === expectedRevision && metadata.digest(directory, inventory(directory, "node_modules").sort((left, right) => left.localeCompare(right, "en"))) === dependencyRevision;
265
569
  };
266
- if (fs.existsSync(target)) {
267
- if (!validate(target)) throw new Error("An existing immutable runtime failed integrity verification");
268
- const currentDependencies = dependencyFiles(root);
269
- if (sourceRevision(root) !== expectedRevision || metadata.digest(root, [...currentDependencies.keys()], currentDependencies) !== dependencyRevision) {
270
- throw new Error("The installation changed during snapshot verification");
271
- }
272
- metadata.flush();
273
- return { directory: target, revision: expectedRevision, key };
274
- }
275
- const temporary = path.join(cache, `.preparing-${process.pid}-${randomUUID()}`);
276
- try {
277
- fs.mkdirSync(temporary, { mode: 0o700 });
278
- const files = ["package.json", ...inventory(root, "src"), ...dependencies.keys()];
279
- if (fs.existsSync(path.join(root, "package-lock.json"))) files.push("package-lock.json");
280
- const directories = new Set(["src", "node_modules", ...files.map((file) => path.dirname(file))]);
281
- for (const directory of directories) fs.mkdirSync(path.join(temporary, directory), { recursive: true, mode: 0o700 });
282
- for (const file of files) {
283
- fs.copyFileSync(dependencies.get(file) ?? path.join(root, file), path.join(temporary, file), fs.constants.COPYFILE_FICLONE);
284
- }
285
- fs.writeFileSync(path.join(temporary, INSTALLATION_LOCKS), installationLocks(root), { flag: "wx", mode: 0o600 });
286
- const currentDependencies = dependencyFiles(root);
287
- if (sourceRevision(root) !== expectedRevision || metadata.digest(root, [...currentDependencies.keys()], currentDependencies) !== dependencyRevision || !validate(temporary)) {
288
- throw new Error("The installation changed while its immutable runtime was being prepared");
289
- }
570
+ const prepare = () => {
571
+ const temporary = path.join(cache, `.preparing-${process.pid}-${randomUUID()}`);
290
572
  try {
291
- fs.renameSync(temporary, target);
292
- metadata.relocate(temporary, target);
293
- }
294
- catch (error) {
295
- if (!fs.existsSync(target) || !validate(target)) throw error;
573
+ const files = ["package.json", ...inventory(root, "src"), ...dependencies.keys()];
574
+ if (fs.existsSync(path.join(root, "package-lock.json"))) files.push("package-lock.json");
575
+ const sources = files.map((file) => [file, dependencies.get(file) ?? path.join(root, file)]);
576
+ const directories = new Set(["src", "node_modules", ...files.map((file) => path.dirname(file))]);
577
+ const layout = () => {
578
+ fs.mkdirSync(temporary, { mode: 0o700 });
579
+ for (const directory of directories) fs.mkdirSync(path.join(temporary, directory), { recursive: true, mode: 0o700 });
580
+ };
581
+ layout();
582
+ let copy = "clonefile";
583
+ if (!cloneSupported() || !cloneRelease(sources, temporary, cache)) {
584
+ copy = "copyfile";
585
+ if (cloneSupported()) { removeOwned(temporary, cache); layout(); }
586
+ for (const [file, source] of sources) fs.copyFileSync(source, path.join(temporary, file), fs.constants.COPYFILE_FICLONE);
587
+ }
588
+ fs.writeFileSync(path.join(temporary, INSTALLATION_LOCKS), installationLocks(root), { flag: "wx", mode: 0o600 });
589
+ const currentDependencies = dependencyFiles(root);
590
+ if (sourceRevision(root) !== expectedRevision || metadata.digest(root, [...currentDependencies.keys()], currentDependencies) !== dependencyRevision || !validate(temporary)) {
591
+ throw new Error("The installation changed while its immutable runtime was being prepared");
592
+ }
593
+ try {
594
+ fs.renameSync(temporary, target);
595
+ metadata.relocate(temporary, target);
596
+ }
597
+ catch (error) {
598
+ if (!fs.existsSync(target) || !validate(target)) throw error;
599
+ removeOwned(temporary, cache);
600
+ metadata.forget(temporary);
601
+ }
602
+ return copy;
603
+ } catch (error) {
296
604
  removeOwned(temporary, cache);
297
- metadata.forget(temporary);
605
+ throw error;
298
606
  }
607
+ };
608
+ const held = lease ? leaseSnapshot(cache, key, target) : null;
609
+ const finish = (copy) => {
299
610
  metadata.flush();
300
- return { directory: target, revision: expectedRevision, key };
611
+ touch(target);
612
+ return { directory: target, revision: expectedRevision, key, copy, ...(held ? { lease: held } : {}) };
613
+ };
614
+ try {
615
+ for (let attempt = 0; fs.existsSync(target); attempt++) {
616
+ try {
617
+ if (!validate(target)) throw new Error("An existing immutable runtime failed integrity verification");
618
+ const currentDependencies = dependencyFiles(root);
619
+ if (sourceRevision(root) !== expectedRevision || metadata.digest(root, [...currentDependencies.keys()], currentDependencies) !== dependencyRevision) {
620
+ throw new Error("The installation changed during snapshot verification");
621
+ }
622
+ return finish("reused");
623
+ } catch (error) {
624
+ if (error?.code !== "ENOENT" || attempt >= 2) throw error;
625
+ }
626
+ }
627
+ return finish(prepare());
301
628
  } catch (error) {
302
- removeOwned(temporary, cache);
629
+ held?.release();
303
630
  throw error;
304
631
  }
305
632
  }