@camstack/agent 1.1.57 → 1.1.59

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.
@@ -65,31 +65,41 @@ var require_dist = __commonJS({
65
65
  var src_exports = {};
66
66
  __export(src_exports, {
67
67
  AGENT_ROOT_SPEC: () => AGENT_ROOT_SPEC,
68
+ CURRENT_DIRNAME: () => CURRENT_DIRNAME,
68
69
  DEV_UPLOADS_DIRNAME: () => DEV_UPLOADS_DIRNAME,
69
70
  DEV_UPLOADS_KEEP_COUNT: () => DEV_UPLOADS_KEEP_COUNT,
70
71
  DEV_UPLOAD_MANIFEST_FILE: () => DEV_UPLOAD_MANIFEST_FILE,
71
72
  HOST_EXTERNAL_SPECIFIERS: () => HOST_EXTERNAL_SPECIFIERS,
72
73
  HUB_ROOT_SPEC: () => HUB_ROOT_SPEC,
74
+ PENDING_ROOT_SWAP_FILE: () => PENDING_ROOT_SWAP_FILE,
75
+ REQUIRED_NATIVE_PACKAGES: () => REQUIRED_NATIVE_PACKAGES,
73
76
  RESTART_INTENT_FILE: () => RESTART_INTENT_FILE,
74
77
  RootUpdateService: () => RootUpdateService,
75
78
  SERVER_ROOT_DIRNAME: () => SERVER_ROOT_DIRNAME,
76
79
  SERVER_ROOT_STATE_FILE: () => SERVER_ROOT_STATE_FILE,
80
+ applyPendingRootSwap: () => applyPendingRootSwap,
81
+ clearPendingRootSwap: () => clearPendingRootSwap,
77
82
  clearRestartIntentMarker: () => clearRestartIntentMarker,
78
83
  compareSemver: () => compareSemver,
84
+ currentDir: () => currentDir,
85
+ currentEntryPath: () => currentEntryPath,
79
86
  detectWorkspaceRoot: () => detectWorkspaceRoot,
80
87
  devChannelEpoch: () => devChannelEpoch,
81
88
  devUploadManifestPath: () => devUploadManifestPath,
82
89
  devUploadVersionDir: () => devUploadVersionDir,
83
90
  devUploadsDir: () => devUploadsDir,
84
91
  emptyServerRootState: () => emptyServerRootState,
92
+ findMissingNativePrebuilds: () => findMissingNativePrebuilds,
85
93
  isDevChannelVersion: () => isDevChannelVersion,
86
94
  isHostExternal: () => isHostExternal,
87
95
  isServerRootState: () => isServerRootState,
96
+ migrateToSingleCopy: () => migrateToSingleCopy,
88
97
  minNodeMajorOf: () => minNodeMajorOf,
98
+ pendingRootSwapPath: () => pendingRootSwapPath,
89
99
  planBoot: () => planBoot,
90
100
  readDevUploadManifest: () => readDevUploadManifest,
101
+ readPendingRootSwap: () => readPendingRootSwap,
91
102
  readRestartIntentMarker: () => readRestartIntentMarker,
92
- readSeedVersion: () => readSeedVersion,
93
103
  readServerRootState: () => readServerRootState,
94
104
  registerActiveRootResolver: () => registerActiveRootResolver,
95
105
  restartIntentMarkerPath: () => restartIntentMarkerPath,
@@ -97,11 +107,15 @@ var require_dist = __commonJS({
97
107
  rootPackageDir: () => rootPackageDir,
98
108
  runNodeStarter: () => runNodeStarter,
99
109
  serverRootDir: () => serverRootDir,
110
+ stagingDirPath: () => stagingDirPath,
100
111
  stateFilePath: () => stateFilePath,
112
+ sweepTransientRootDirs: () => sweepTransientRootDirs,
113
+ validateClosureDir: () => validateClosureDir,
101
114
  validateVersionDir: () => validateVersionDir,
102
115
  versionDir: () => versionDir,
103
116
  versionsDir: () => versionsDir,
104
117
  writeDevUploadManifest: () => writeDevUploadManifest,
118
+ writePendingRootSwap: () => writePendingRootSwap,
105
119
  writeRestartIntentMarker: () => writeRestartIntentMarker,
106
120
  writeServerRootState: () => writeServerRootState
107
121
  });
@@ -266,13 +280,12 @@ var require_dist = __commonJS({
266
280
  const major = Number.parseInt(match[1] ?? "", 10);
267
281
  return Number.isNaN(major) ? null : major;
268
282
  }
269
- function validateVersionDir(rootDir, version, nodeMajor, spec) {
270
- const vDir = versionDir(rootDir, version);
271
- const entry = rootEntryPath(vDir, spec);
283
+ function validateClosureDir(closureDir, nodeMajor, spec, expectedVersion) {
284
+ const entry = rootEntryPath(closureDir, spec);
272
285
  if (!fs2.existsSync(entry)) {
273
286
  return `root entry missing: ${entry}`;
274
287
  }
275
- const pkgJsonPath = path2.join(rootPackageDir(vDir, spec), "package.json");
288
+ const pkgJsonPath = path2.join(rootPackageDir(closureDir, spec), "package.json");
276
289
  let pkg;
277
290
  try {
278
291
  const raw = JSON.parse(fs2.readFileSync(pkgJsonPath, "utf-8"));
@@ -286,8 +299,8 @@ var require_dist = __commonJS({
286
299
  if (pkg["name"] !== spec.packageName) {
287
300
  return `package name mismatch: expected ${spec.packageName}, got ${String(pkg["name"])}`;
288
301
  }
289
- if (pkg["version"] !== version) {
290
- return `package version mismatch: dir says ${version}, package.json says ${String(pkg["version"])}`;
302
+ if (expectedVersion !== void 0 && pkg["version"] !== expectedVersion) {
303
+ return `package version mismatch: expected ${expectedVersion}, package.json says ${String(pkg["version"])}`;
291
304
  }
292
305
  const engines = pkg["engines"];
293
306
  if (typeof engines === "object" && engines !== null) {
@@ -301,6 +314,11 @@ var require_dist = __commonJS({
301
314
  }
302
315
  return null;
303
316
  }
317
+ function validateVersionDir(rootDir, version, nodeMajor, spec) {
318
+ return validateClosureDir(versionDir(rootDir, version), nodeMajor, spec, version);
319
+ }
320
+ var fs3 = __toESM2(__require("fs"));
321
+ var path3 = __toESM2(__require("path"));
304
322
  function parse(version) {
305
323
  const dashIdx = version.indexOf("-");
306
324
  const base = dashIdx === -1 ? version : version.slice(0, dashIdx);
@@ -328,133 +346,268 @@ var require_dist = __commonJS({
328
346
  if (pa.prerelease > pb.prerelease) return 1;
329
347
  return 0;
330
348
  }
331
- function planBoot(state, isValidVersion, now, seedVersion = null) {
332
- if (state === null) {
333
- return { kind: "baked", reason: "no server-root state", stateToWrite: null };
349
+ var CURRENT_DIRNAME = "current";
350
+ var PENDING_ROOT_SWAP_FILE = ".pending-root-swap.json";
351
+ var LEGACY_FRAMEWORK_MARKERS = [
352
+ ".pending-framework-swap.json",
353
+ ".framework-swap-confirm.json"
354
+ ];
355
+ function currentDir(rootDir) {
356
+ return path3.join(rootDir, CURRENT_DIRNAME);
357
+ }
358
+ function currentEntryPath(rootDir, spec) {
359
+ return rootEntryPath(currentDir(rootDir), spec);
360
+ }
361
+ function stagingDirPath(rootDir, target, pid, now) {
362
+ return path3.join(rootDir, `.staging-${target}-${pid}-${now}`);
363
+ }
364
+ function pendingRootSwapPath(rootDir) {
365
+ return path3.join(rootDir, PENDING_ROOT_SWAP_FILE);
366
+ }
367
+ function isPendingRootSwap(v) {
368
+ if (typeof v !== "object" || v === null) return false;
369
+ const m = v;
370
+ return m["schemaVersion"] === 1 && typeof m["version"] === "string" && typeof m["stagingPath"] === "string" && typeof m["requestedAtMs"] === "number";
371
+ }
372
+ function readPendingRootSwap(rootDir) {
373
+ try {
374
+ const raw = JSON.parse(fs3.readFileSync(pendingRootSwapPath(rootDir), "utf-8"));
375
+ return isPendingRootSwap(raw) ? raw : null;
376
+ } catch {
377
+ return null;
334
378
  }
335
- let next = state;
336
- let changed = false;
337
- if (next.pendingBoot !== null) {
338
- const pending = next.pendingBoot;
339
- if (pending.bootAttempts >= 1) {
340
- next = {
341
- ...next,
342
- pendingBoot: null,
343
- rolledBack: {
344
- fromVersion: pending.version,
345
- toVersion: next.currentVersion,
346
- atMs: now(),
347
- reason: "probation boot did not reach ready \u2014 rolled back"
348
- }
349
- };
350
- changed = true;
351
- } else if (!isValidVersion(pending.version)) {
352
- next = {
353
- ...next,
354
- pendingBoot: null,
355
- rolledBack: {
356
- fromVersion: pending.version,
357
- toVersion: next.currentVersion,
358
- atMs: now(),
359
- reason: "staged version failed validation"
360
- }
361
- };
362
- changed = true;
363
- } else {
364
- return {
365
- kind: "data-root",
366
- version: pending.version,
367
- probation: true,
368
- stateToWrite: {
369
- ...next,
370
- pendingBoot: { ...pending, bootAttempts: pending.bootAttempts + 1 }
371
- }
372
- };
373
- }
379
+ }
380
+ function writePendingRootSwap(rootDir, marker) {
381
+ fs3.mkdirSync(rootDir, { recursive: true });
382
+ const target = pendingRootSwapPath(rootDir);
383
+ const tmp = `${target}.tmp`;
384
+ fs3.writeFileSync(tmp, JSON.stringify(marker, null, 2), "utf-8");
385
+ fs3.renameSync(tmp, target);
386
+ }
387
+ function clearPendingRootSwap(rootDir) {
388
+ try {
389
+ fs3.rmSync(pendingRootSwapPath(rootDir), { force: true });
390
+ } catch {
374
391
  }
375
- if (next.currentVersion !== null) {
376
- if (isValidVersion(next.currentVersion)) {
377
- if (!changed && seedVersion !== null && compareSemver(seedVersion, next.currentVersion) > 0) {
378
- return {
379
- kind: "baked",
380
- reason: `baked seed ${seedVersion} is newer than active data-root ${next.currentVersion} \u2014 adopting the image seed`,
381
- stateToWrite: {
382
- ...next,
383
- currentVersion: null,
384
- previousVersion: null,
385
- pendingBoot: null,
386
- rolledBack: null
387
- }
388
- };
389
- }
390
- return {
391
- kind: "data-root",
392
- version: next.currentVersion,
393
- probation: false,
394
- stateToWrite: changed ? next : null
395
- };
396
- }
397
- const broken = next.currentVersion;
398
- if (next.previousVersion !== null && isValidVersion(next.previousVersion)) {
399
- const fallback = next.previousVersion;
400
- return {
401
- kind: "data-root",
402
- version: fallback,
403
- probation: false,
404
- stateToWrite: {
405
- ...next,
406
- currentVersion: fallback,
407
- previousVersion: null,
408
- rolledBack: {
409
- fromVersion: broken,
410
- toVersion: fallback,
411
- atMs: now(),
412
- reason: "active version failed validation"
413
- }
414
- }
415
- };
392
+ }
393
+ var REQUIRED_NATIVE_PACKAGES = ["better-sqlite3", "sharp", "node-av"];
394
+ var NATIVE_SEARCH_DIRS = {
395
+ "better-sqlite3": ["better-sqlite3"],
396
+ sharp: ["sharp", "@img"],
397
+ "node-av": ["node-av", "@seydx"]
398
+ };
399
+ function hasDotNode(dir, maxDepth) {
400
+ let entries;
401
+ try {
402
+ entries = fs3.readdirSync(dir, { withFileTypes: true });
403
+ } catch {
404
+ return false;
405
+ }
406
+ for (const entry of entries) {
407
+ if (entry.isFile() && entry.name.endsWith(".node")) return true;
408
+ }
409
+ if (maxDepth <= 0) return false;
410
+ for (const entry of entries) {
411
+ if (entry.isDirectory() && hasDotNode(path3.join(dir, entry.name), maxDepth - 1)) return true;
412
+ }
413
+ return false;
414
+ }
415
+ function findMissingNativePrebuilds(closureDir) {
416
+ const nm = path3.join(closureDir, "node_modules");
417
+ const missing = [];
418
+ for (const pkg of REQUIRED_NATIVE_PACKAGES) {
419
+ const searchDirs = NATIVE_SEARCH_DIRS[pkg] ?? [pkg];
420
+ const found = searchDirs.some((rel) => hasDotNode(path3.join(nm, ...rel.split("/")), 4));
421
+ if (!found) missing.push(pkg);
422
+ }
423
+ return missing;
424
+ }
425
+ function rmrf(target) {
426
+ try {
427
+ fs3.rmSync(target, { recursive: true, force: true });
428
+ } catch {
429
+ }
430
+ }
431
+ function errMsg(err) {
432
+ return err instanceof Error ? err.message : String(err);
433
+ }
434
+ function applyPendingRootSwap(rootDir, spec, nodeMajor, now, log) {
435
+ const marker = readPendingRootSwap(rootDir);
436
+ if (marker === null) return { applied: false, version: null, reason: "no pending swap" };
437
+ const staging = marker.stagingPath;
438
+ const dest = currentDir(rootDir);
439
+ const invalid = validateClosureDir(staging, nodeMajor, spec, marker.version);
440
+ if (invalid !== null) {
441
+ if (!fs3.existsSync(staging) && validateClosureDir(dest, nodeMajor, spec, marker.version) === null) {
442
+ clearPendingRootSwap(rootDir);
443
+ log(`[single-copy] pending swap ${marker.version} already applied \u2014 cleared marker`);
444
+ return { applied: true, version: marker.version, reason: "already applied" };
416
445
  }
446
+ log(`[single-copy] staged swap ${marker.version} invalid \u2014 discarding (${invalid})`);
447
+ rmrf(staging);
448
+ clearPendingRootSwap(rootDir);
449
+ return { applied: false, version: null, reason: invalid };
450
+ }
451
+ const missing = findMissingNativePrebuilds(staging);
452
+ if (missing.length > 0) {
453
+ log(
454
+ `[single-copy] staged swap ${marker.version} missing native prebuilds: ${missing.join(", ")} \u2014 discarding`
455
+ );
456
+ rmrf(staging);
457
+ clearPendingRootSwap(rootDir);
417
458
  return {
418
- kind: "baked",
419
- reason: `active version ${broken} failed validation and no valid previous version exists`,
420
- stateToWrite: {
421
- ...next,
422
- currentVersion: null,
423
- rolledBack: {
424
- fromVersion: broken,
425
- toVersion: null,
426
- atMs: now(),
427
- reason: "active version failed validation"
459
+ applied: false,
460
+ version: null,
461
+ reason: `missing native prebuilds: ${missing.join(", ")}`
462
+ };
463
+ }
464
+ let trash = null;
465
+ try {
466
+ if (fs3.existsSync(dest)) {
467
+ trash = path3.join(rootDir, `.trash-${now()}-${process.pid}`);
468
+ fs3.renameSync(dest, trash);
469
+ }
470
+ try {
471
+ fs3.renameSync(staging, dest);
472
+ } catch (err) {
473
+ if (trash !== null && !fs3.existsSync(dest)) {
474
+ try {
475
+ fs3.renameSync(trash, dest);
476
+ } catch {
428
477
  }
429
478
  }
430
- };
479
+ throw err;
480
+ }
481
+ } catch (err) {
482
+ log(`[single-copy] swap of ${marker.version} FAILED \u2014 current preserved (${errMsg(err)})`);
483
+ return { applied: false, version: null, reason: errMsg(err) };
431
484
  }
432
- return {
433
- kind: "baked",
434
- reason: "no active data-root version",
435
- stateToWrite: changed ? next : null
436
- };
485
+ if (trash !== null) rmrf(trash);
486
+ clearPendingRootSwap(rootDir);
487
+ log(`[single-copy] applied staged root swap \u2192 ${marker.version}`);
488
+ return { applied: true, version: marker.version, reason: "applied" };
437
489
  }
438
- var fs3 = __toESM2(__require("fs"));
439
- var path3 = __toESM2(__require("path"));
490
+ function readClosureVersion(closureDir, spec) {
491
+ try {
492
+ const raw = JSON.parse(
493
+ fs3.readFileSync(path3.join(rootPackageDir(closureDir, spec), "package.json"), "utf-8")
494
+ );
495
+ if (typeof raw === "object" && raw !== null) {
496
+ const version = raw["version"];
497
+ if (typeof version === "string") return version;
498
+ }
499
+ } catch {
500
+ }
501
+ return null;
502
+ }
503
+ function isAdoptablePendingClosure(rootDir, version, nodeMajor, spec) {
504
+ const dir = versionDir(rootDir, version);
505
+ return validateClosureDir(dir, nodeMajor, spec, version) === null && findMissingNativePrebuilds(dir).length === 0;
506
+ }
507
+ function migrateToSingleCopy(rootDir, spec, nodeMajor, now, log) {
508
+ const dest = currentDir(rootDir);
509
+ const legacyState = readServerRootState(rootDir);
510
+ const pendingVersion = legacyState?.pendingBoot?.version ?? null;
511
+ const pendingAdoptable = pendingVersion !== null && isAdoptablePendingClosure(rootDir, pendingVersion, nodeMajor, spec);
512
+ const currentValid = fs3.existsSync(dest) && validateClosureDir(dest, nodeMajor, spec) === null;
513
+ if (currentValid) {
514
+ const currentVersion = readClosureVersion(dest, spec);
515
+ const pendingSupersedes = pendingAdoptable && pendingVersion !== null && (currentVersion === null || compareSemver(pendingVersion, currentVersion) > 0);
516
+ if (!pendingSupersedes) {
517
+ return { migrated: false, version: null, reason: "current already present" };
518
+ }
519
+ log(
520
+ `[single-copy] in-flight pending ${pendingVersion} supersedes older current ${currentVersion ?? "?"}`
521
+ );
522
+ }
523
+ const candidates = [];
524
+ if (pendingAdoptable && pendingVersion !== null) candidates.push(pendingVersion);
525
+ if (legacyState !== null) {
526
+ for (const v of [
527
+ legacyState.currentVersion,
528
+ legacyState.pendingBoot?.version ?? null,
529
+ legacyState.previousVersion
530
+ ]) {
531
+ if (typeof v === "string" && !candidates.includes(v)) candidates.push(v);
532
+ }
533
+ }
534
+ try {
535
+ for (const entry of fs3.readdirSync(versionsDir(rootDir))) {
536
+ if (!entry.startsWith(".") && !candidates.includes(entry)) candidates.push(entry);
537
+ }
538
+ } catch {
539
+ }
540
+ const source = candidates.find(
541
+ (v) => validateClosureDir(versionDir(rootDir, v), nodeMajor, spec, v) === null
542
+ );
543
+ if (source === void 0) {
544
+ return { migrated: false, version: null, reason: "no valid legacy versions/<X> to adopt" };
545
+ }
546
+ if (fs3.existsSync(dest)) {
547
+ const aside = path3.join(rootDir, `.trash-${now()}-${process.pid}-stale-current`);
548
+ try {
549
+ fs3.renameSync(dest, aside);
550
+ } catch {
551
+ rmrf(dest);
552
+ }
553
+ }
554
+ try {
555
+ fs3.renameSync(versionDir(rootDir, source), dest);
556
+ } catch (err) {
557
+ log(`[single-copy] migration rename of versions/${source} failed (${errMsg(err)})`);
558
+ return { migrated: false, version: null, reason: errMsg(err) };
559
+ }
560
+ rmrf(versionsDir(rootDir));
561
+ rmrf(stateFilePath(rootDir));
562
+ const dataDir = path3.dirname(rootDir);
563
+ for (const marker of LEGACY_FRAMEWORK_MARKERS) rmrf(path3.join(dataDir, marker));
564
+ log(`[single-copy] migrated legacy versions/${source} \u2192 current`);
565
+ return { migrated: true, version: source, reason: "adopted legacy version" };
566
+ }
567
+ function sweepTransientRootDirs(rootDir, now, staleMs) {
568
+ let entries;
569
+ try {
570
+ entries = fs3.readdirSync(rootDir);
571
+ } catch {
572
+ return;
573
+ }
574
+ for (const entry of entries) {
575
+ if (!entry.startsWith(".staging-") && !entry.startsWith(".trash-")) continue;
576
+ const full = path3.join(rootDir, entry);
577
+ try {
578
+ if (now - fs3.statSync(full).mtimeMs < staleMs) continue;
579
+ } catch {
580
+ continue;
581
+ }
582
+ rmrf(full);
583
+ }
584
+ }
585
+ function planBoot(currentValid) {
586
+ if (currentValid) {
587
+ return { kind: "current", reason: "single-copy closure present and valid" };
588
+ }
589
+ return { kind: "baked", reason: "no valid single-copy closure \u2014 booting the baked seed" };
590
+ }
591
+ var fs4 = __toESM2(__require("fs"));
592
+ var path4 = __toESM2(__require("path"));
440
593
  function detectWorkspaceRoot(fromDir) {
441
- let dir = path3.resolve(fromDir);
594
+ let dir = path4.resolve(fromDir);
442
595
  for (; ; ) {
443
- const pkgPath = path3.join(dir, "package.json");
596
+ const pkgPath = path4.join(dir, "package.json");
444
597
  try {
445
- const raw = JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
598
+ const raw = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
446
599
  if (typeof raw === "object" && raw !== null && raw["workspaces"] !== void 0) {
447
600
  return dir;
448
601
  }
449
602
  } catch {
450
603
  }
451
- const parent = path3.dirname(dir);
604
+ const parent = path4.dirname(dir);
452
605
  if (parent === dir) return null;
453
606
  dir = parent;
454
607
  }
455
608
  }
456
- var fs4 = __toESM2(__require("fs"));
457
- var path4 = __toESM2(__require("path"));
609
+ var fs5 = __toESM2(__require("fs"));
610
+ var path5 = __toESM2(__require("path"));
458
611
  var import_node_url = __require("url");
459
612
  var HOST_EXTERNAL_SPECIFIERS = [
460
613
  "@camstack/system",
@@ -482,7 +635,7 @@ var require_dist = __commonJS({
482
635
  return;
483
636
  }
484
637
  const anchorURL = (0, import_node_url.pathToFileURL)(
485
- path4.join(activeRootDir, "node_modules", "__camstack_starter_anchor__.js")
638
+ path5.join(activeRootDir, "node_modules", "__camstack_starter_anchor__.js")
486
639
  ).href;
487
640
  const hooks = {
488
641
  resolve: (specifier, context, nextResolve) => {
@@ -497,23 +650,24 @@ var require_dist = __commonJS({
497
650
  };
498
651
  registerHooks(hooks);
499
652
  }
500
- function readSeedVersion(seedEntry) {
653
+ function readClosureVersion2(closureDir, spec) {
501
654
  try {
502
- const pkgJsonPath = path4.join(path4.dirname(seedEntry), "..", "package.json");
503
- const raw = JSON.parse(fs4.readFileSync(pkgJsonPath, "utf-8"));
655
+ const raw = JSON.parse(
656
+ fs5.readFileSync(path5.join(rootPackageDir(closureDir, spec), "package.json"), "utf-8")
657
+ );
504
658
  if (typeof raw === "object" && raw !== null) {
505
659
  const version = raw["version"];
506
660
  if (typeof version === "string") return version;
507
661
  }
508
- return null;
509
662
  } catch {
510
- return null;
511
663
  }
664
+ return null;
512
665
  }
513
666
  function runNodeStarter(options) {
514
667
  const env = options.env ?? process.env;
515
668
  const now = options.now ?? Date.now;
516
669
  const registerResolver = options.registerResolver ?? registerActiveRootResolver;
670
+ const nodeMajor = options.nodeMajor ?? Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
517
671
  const disabled = env[options.envNames.killSwitch] === "off";
518
672
  const workspaceRoot = detectWorkspaceRoot(options.starterDir);
519
673
  if (workspaceRoot !== null) {
@@ -531,40 +685,34 @@ var require_dist = __commonJS({
531
685
  return;
532
686
  }
533
687
  const rootDir = serverRootDir(options.dataDir);
534
- const state = readServerRootState(rootDir);
535
- const nodeMajor = options.nodeMajor ?? Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
536
- const isValidVersion = (version) => {
537
- const reason = validateVersionDir(rootDir, version, nodeMajor, options.spec);
538
- if (reason !== null) console.warn(`[starter] version ${version} invalid: ${reason}`);
539
- return reason === null;
540
- };
541
- const seedVersion = readSeedVersion(options.seedEntry);
542
- const plan = planBoot(state, isValidVersion, now, seedVersion);
543
- if (plan.stateToWrite !== null) {
544
- try {
545
- writeServerRootState(rootDir, plan.stateToWrite);
546
- } catch (err) {
547
- console.error("[starter] FAILED to persist server-root state \u2014 booting baked seed:", err);
548
- env[options.envNames.bootMode] = "baked";
549
- options.loadEntry(options.seedEntry);
550
- return;
551
- }
552
- if (plan.stateToWrite.rolledBack !== null && (state?.rolledBack ?? null) !== plan.stateToWrite.rolledBack) {
553
- const rb = plan.stateToWrite.rolledBack;
554
- console.warn(
555
- `[starter] ROLLED BACK ${rb.fromVersion} -> ${rb.toVersion ?? "baked seed"}: ${rb.reason}`
556
- );
557
- }
688
+ const swap = applyPendingRootSwap(rootDir, options.spec, nodeMajor, now, (m) => console.log(m));
689
+ if (swap.applied) console.log(`[starter] applied staged update \u2192 ${swap.version ?? "?"}`);
690
+ else if (swap.reason !== "no pending swap")
691
+ console.warn(`[starter] staged update NOT applied: ${swap.reason}`);
692
+ const migration = migrateToSingleCopy(
693
+ rootDir,
694
+ options.spec,
695
+ nodeMajor,
696
+ now,
697
+ (m) => console.log(m)
698
+ );
699
+ if (migration.migrated)
700
+ console.log(`[starter] migrated legacy layout \u2192 single copy (${migration.version ?? "?"})`);
701
+ const activeRootDir = currentDir(rootDir);
702
+ const invalid = validateClosureDir(activeRootDir, nodeMajor, options.spec);
703
+ if (invalid !== null && fs5.existsSync(activeRootDir)) {
704
+ console.warn(`[starter] single copy invalid: ${invalid}`);
558
705
  }
559
- if (plan.kind === "data-root") {
560
- const activeRootDir = versionDir(rootDir, plan.version);
561
- const entry = rootEntryPath(activeRootDir, options.spec);
706
+ const plan = planBoot(invalid === null);
707
+ if (plan.kind === "current") {
708
+ const version = readClosureVersion2(activeRootDir, options.spec);
709
+ const entry = currentEntryPath(rootDir, options.spec);
562
710
  console.log(
563
- `[starter] booting ${options.spec.packageName}@${plan.version} from ${activeRootDir}` + (plan.probation ? " (probation boot \u2014 health-check armed)" : "")
711
+ `[starter] booting ${options.spec.packageName}@${version ?? "?"} from ${activeRootDir}`
564
712
  );
565
713
  env[options.envNames.bootMode] = "data-root";
566
714
  env[options.envNames.activeRoot] = activeRootDir;
567
- env[options.envNames.activeVersion] = plan.version;
715
+ if (version !== null) env[options.envNames.activeVersion] = version;
568
716
  registerResolver(activeRootDir);
569
717
  options.loadEntry(entry);
570
718
  return;
@@ -574,8 +722,8 @@ var require_dist = __commonJS({
574
722
  options.loadEntry(options.seedEntry);
575
723
  }
576
724
  var import_node_child_process = __require("child_process");
577
- var fs5 = __toESM2(__require("fs"));
578
- var path5 = __toESM2(__require("path"));
725
+ var fs6 = __toESM2(__require("fs"));
726
+ var path6 = __toESM2(__require("path"));
579
727
  var import_node_util = __require("util");
580
728
  var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
581
729
  function buildNpmRegistryArgs(registry) {
@@ -585,9 +733,10 @@ var require_dist = __commonJS({
585
733
  var NPM_VIEW_TIMEOUT_MS = 2e4;
586
734
  var NPM_INSTALL_TIMEOUT_MS = 15 * 6e4;
587
735
  var RESTART_REASON_PREFIX = "server-update";
736
+ var STALE_TRANSIENT_MS = 24 * 60 * 60 * 1e3;
588
737
  function readPackageVersion(pkgJsonPath) {
589
738
  try {
590
- const raw = JSON.parse(fs5.readFileSync(pkgJsonPath, "utf-8"));
739
+ const raw = JSON.parse(fs6.readFileSync(pkgJsonPath, "utf-8"));
591
740
  if (typeof raw === "object" && raw !== null) {
592
741
  const version = raw["version"];
593
742
  if (typeof version === "string") return version;
@@ -596,7 +745,7 @@ var require_dist = __commonJS({
596
745
  }
597
746
  return null;
598
747
  }
599
- var RootUpdateService = class _RootUpdateService {
748
+ var RootUpdateService = class {
600
749
  spec;
601
750
  envNames;
602
751
  logger;
@@ -614,7 +763,7 @@ var require_dist = __commonJS({
614
763
  this.envNames = options.envNames;
615
764
  this.logger = options.logger;
616
765
  this.restartServerFn = options.restartServer;
617
- this.dataDir = path5.resolve(options.dataDir);
766
+ this.dataDir = path6.resolve(options.dataDir);
618
767
  this.execNpm = options.execNpm ?? (async (args, opts) => {
619
768
  const { stdout } = await execFileAsync("npm", [...args], {
620
769
  cwd: opts.cwd,
@@ -643,57 +792,37 @@ var require_dist = __commonJS({
643
792
  seedVersion() {
644
793
  const seedDir = this.env[this.envNames.seedDir];
645
794
  if (seedDir === void 0 || seedDir.length === 0) return null;
646
- return readPackageVersion(path5.join(seedDir, "package.json"));
795
+ return readPackageVersion(path6.join(seedDir, "package.json"));
647
796
  }
648
797
  rootDir() {
649
798
  return serverRootDir(this.dataDir);
650
799
  }
651
- readState() {
652
- return readServerRootState(this.rootDir()) ?? emptyServerRootState();
653
- }
654
- /**
655
- * Like `readState`, but distinguishes "state.json missing" (normal on a
656
- * fresh node) from "state.json present but corrupt" — in the corrupt case
657
- * the starter booted the baked seed while installed data-dir versions are
658
- * silently ignored, which the status surface must make visible.
659
- */
660
- readStateInfo() {
661
- const rootDir = this.rootDir();
662
- const raw = readServerRootState(rootDir);
663
- if (raw !== null) return { state: raw, corrupt: false };
664
- return { state: emptyServerRootState(), corrupt: fs5.existsSync(stateFilePath(rootDir)) };
665
- }
666
- updateState(state) {
800
+ updateState() {
667
801
  if (this.inFlight === "checking") return "checking";
668
802
  if (this.inFlight === "staging") return "staging";
669
- if (state.pendingBoot !== null) {
670
- const activeVersion = this.env[this.envNames.activeVersion] ?? null;
671
- if (activeVersion !== null && activeVersion === state.pendingBoot.version) {
672
- return "awaiting-confirmation";
673
- }
674
- return "pending-restart";
675
- }
803
+ if (readPendingRootSwap(this.rootDir()) !== null) return "pending-restart";
676
804
  return "idle";
677
805
  }
678
806
  async getServerPackageStatus() {
679
- const { state, corrupt } = this.readStateInfo();
680
807
  const runningVersion = this.runningVersion();
681
808
  const latestVersion = this.checkCache?.latestVersion ?? null;
809
+ const pending = readPendingRootSwap(this.rootDir());
682
810
  const updateAvailable = latestVersion !== null && runningVersion !== null && compareSemver(latestVersion, runningVersion) > 0;
683
811
  return {
684
812
  packageName: this.spec.packageName,
685
813
  runningVersion,
686
814
  nodeRuntimeVersion: process.versions.node,
687
- activeVersion: this.env[this.envNames.activeVersion] ?? state.currentVersion,
688
- previousVersion: state.previousVersion,
815
+ activeVersion: this.env[this.envNames.activeVersion] ?? runningVersion,
816
+ // Single-copy model: no N-1 retention, no rollback record.
817
+ previousVersion: null,
689
818
  seedVersion: this.seedVersion(),
690
819
  latestVersion,
691
820
  updateAvailable,
692
821
  bootMode: this.resolveBootMode(),
693
- updateState: this.updateState(state),
694
- pendingVersion: state.pendingBoot?.version ?? null,
695
- rolledBack: state.rolledBack,
696
- stateFileCorrupt: corrupt,
822
+ updateState: this.updateState(),
823
+ pendingVersion: pending?.version ?? null,
824
+ rolledBack: null,
825
+ stateFileCorrupt: false,
697
826
  lastCheckedAtMs: this.checkCache?.checkedAtMs ?? null
698
827
  };
699
828
  }
@@ -770,13 +899,13 @@ var require_dist = __commonJS({
770
899
  message: `Refused: another operation is in flight (${this.inFlight}).`
771
900
  };
772
901
  }
773
- const stateBefore = this.readState();
774
- if (stateBefore.pendingBoot !== null) {
902
+ const pendingBefore = readPendingRootSwap(this.rootDir());
903
+ if (pendingBefore !== null) {
775
904
  return {
776
905
  accepted: false,
777
906
  targetVersion: input.version ?? null,
778
907
  restarting: false,
779
- message: `Refused: version ${stateBefore.pendingBoot.version} is already staged and awaiting restart.`
908
+ message: `Refused: version ${pendingBefore.version} is already staged and awaiting restart.`
780
909
  };
781
910
  }
782
911
  let target = input.version ?? null;
@@ -803,7 +932,7 @@ var require_dist = __commonJS({
803
932
  }
804
933
  if (isDevChannelVersion(target)) {
805
934
  const uploadsDir = devUploadVersionDir(this.rootDir(), target);
806
- if (!fs5.existsSync(uploadsDir)) {
935
+ if (!fs6.existsSync(uploadsDir)) {
807
936
  return {
808
937
  accepted: false,
809
938
  targetVersion: target,
@@ -813,8 +942,9 @@ var require_dist = __commonJS({
813
942
  }
814
943
  }
815
944
  this.inFlight = "staging";
945
+ let stagingPath;
816
946
  try {
817
- await this.stageAndActivate(target);
947
+ stagingPath = await this.stageClosure(target);
818
948
  } catch (err) {
819
949
  const message = err instanceof Error ? err.message : String(err);
820
950
  this.logger.error("root package update staging failed", {
@@ -829,45 +959,42 @@ var require_dist = __commonJS({
829
959
  } finally {
830
960
  this.inFlight = "idle";
831
961
  }
832
- const state = this.readState();
833
- writeServerRootState(this.rootDir(), {
834
- ...state,
835
- pendingBoot: {
836
- version: target,
837
- fromVersion: state.currentVersion,
838
- requestedAtMs: this.now(),
839
- bootAttempts: 0
840
- },
841
- rolledBack: null
962
+ writePendingRootSwap(this.rootDir(), {
963
+ schemaVersion: 1,
964
+ version: target,
965
+ stagingPath,
966
+ requestedAtMs: this.now()
842
967
  });
843
968
  this.logger.info("root package update staged \u2014 restarting to apply", {
844
- meta: { targetVersion: target, fromVersion: state.currentVersion }
969
+ meta: { targetVersion: target, fromVersion: runningVersion }
845
970
  });
846
971
  this.restartServerFn(`${RESTART_REASON_PREFIX}: ${this.spec.packageName}@${target}`);
847
972
  return {
848
973
  accepted: true,
849
974
  targetVersion: target,
850
975
  restarting: true,
851
- message: `Staged ${this.spec.packageName}@${target} \u2014 restarting to apply (probation boot with auto-rollback).`
976
+ message: `Staged ${this.spec.packageName}@${target} \u2014 restarting to apply (single automatic reboot).`
852
977
  };
853
978
  }
854
979
  /**
855
- * npm-install the target closure into a SAME-FS staging dir inside
856
- * `versions/`, validate it, then atomically rename it into place.
980
+ * npm-install the target closure into a SAME-FS `.staging-*` dir, then
981
+ * validate it (entry + version + native prebuilds). Returns the staging dir
982
+ * path for the pending-root-swap marker; the STARTER swaps it into `current`
983
+ * on the next boot. Throws (cleaning up the staging dir) on any failure so
984
+ * `applyServerUpdate` reports a staging error and never arms a bad swap.
857
985
  */
858
- async stageAndActivate(target) {
986
+ async stageClosure(target) {
859
987
  const rootDir = this.rootDir();
860
- const vDir = versionsDir(rootDir);
861
- fs5.mkdirSync(vDir, { recursive: true });
862
- const stagingDir = path5.join(vDir, `.staging-${target}-${process.pid}-${this.now()}`);
863
- fs5.mkdirSync(stagingDir, { recursive: true });
988
+ fs6.mkdirSync(rootDir, { recursive: true });
989
+ const stagingDir = stagingDirPath(rootDir, target, process.pid, this.now());
990
+ fs6.mkdirSync(stagingDir, { recursive: true });
864
991
  try {
865
992
  const devDir = devUploadVersionDir(rootDir, target);
866
993
  const registry = this.env["CAMSTACK_NPM_REGISTRY"];
867
994
  const installArgs = ["install", "--omit=dev", "--no-audit", "--no-fund", "--loglevel=error"];
868
- if (fs5.existsSync(devDir)) {
869
- fs5.writeFileSync(
870
- path5.join(stagingDir, "package.json"),
995
+ if (fs6.existsSync(devDir)) {
996
+ fs6.writeFileSync(
997
+ path6.join(stagingDir, "package.json"),
871
998
  JSON.stringify(this.buildDevUploadsPackageJson(devDir, target), null, 2),
872
999
  "utf-8"
873
1000
  );
@@ -876,8 +1003,8 @@ var require_dist = __commonJS({
876
1003
  timeoutMs: NPM_INSTALL_TIMEOUT_MS
877
1004
  });
878
1005
  } else {
879
- fs5.writeFileSync(
880
- path5.join(stagingDir, "package.json"),
1006
+ fs6.writeFileSync(
1007
+ path6.join(stagingDir, "package.json"),
881
1008
  JSON.stringify({ name: "camstack-node-root", private: true }, null, 2),
882
1009
  "utf-8"
883
1010
  );
@@ -887,37 +1014,32 @@ var require_dist = __commonJS({
887
1014
  );
888
1015
  }
889
1016
  const entry = rootEntryPath(stagingDir, this.spec);
890
- if (!fs5.existsSync(entry)) {
1017
+ if (!fs6.existsSync(entry)) {
891
1018
  throw new Error(`staged closure is missing the root entry (${entry})`);
892
1019
  }
893
- const stagedVersion = readPackageVersion(
894
- path5.join(rootPackageDir(stagingDir, this.spec), "package.json")
895
- );
896
- if (stagedVersion !== target) {
1020
+ const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
1021
+ const invalid = validateClosureDir(stagingDir, nodeMajor, this.spec, target);
1022
+ if (invalid !== null) {
1023
+ throw new Error(`staged closure invalid: ${invalid}`);
1024
+ }
1025
+ const missingNatives = findMissingNativePrebuilds(stagingDir);
1026
+ if (missingNatives.length > 0) {
897
1027
  throw new Error(
898
- `staged closure version mismatch: expected ${target}, got ${stagedVersion ?? "unknown"}`
1028
+ `staged closure missing native prebuilds: ${missingNatives.join(", ")} \u2014 refusing to arm a swap that would break the forked runners`
899
1029
  );
900
1030
  }
901
- const dest = versionDir(rootDir, target);
902
- if (fs5.existsSync(dest)) {
903
- const aside = `${dest}.evicted-${this.now()}`;
904
- await fs5.promises.rename(dest, aside);
905
- await fs5.promises.rm(aside, { recursive: true, force: true }).catch(() => void 0);
906
- }
907
- await fs5.promises.rename(stagingDir, dest);
1031
+ return stagingDir;
908
1032
  } catch (err) {
909
- await fs5.promises.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
1033
+ await fs6.promises.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
910
1034
  throw err;
911
1035
  }
912
1036
  }
913
1037
  /**
914
1038
  * Synthetic staging package.json for the dev server-deploy channel:
915
1039
  * `dependencies` carries the root package as a `file:` ref to its uploaded
916
- * tgz; `overrides` maps EVERY OTHER uploaded `@camstack/<name>` to its tgz
917
- * so any transitive occurrence resolves to the uploaded copy, never npm.
1040
+ * tgz; `overrides` maps EVERY OTHER uploaded `@camstack/<name>` to its tgz.
918
1041
  * Fail-closed: a missing/invalid manifest, a manifest for a different
919
- * version, a missing root package, or a listed-but-absent tgz all throw
920
- * (surfaced by `applyServerUpdate` as a staging failure).
1042
+ * version, a missing root package, or a listed-but-absent tgz all throw.
921
1043
  */
922
1044
  buildDevUploadsPackageJson(devDir, target) {
923
1045
  const manifest = readDevUploadManifest(devDir);
@@ -936,8 +1058,8 @@ var require_dist = __commonJS({
936
1058
  );
937
1059
  }
938
1060
  const fileRef = (filename) => {
939
- const abs = path5.join(devDir, filename);
940
- if (!fs5.existsSync(abs)) {
1061
+ const abs = path6.join(devDir, filename);
1062
+ if (!fs6.existsSync(abs)) {
941
1063
  throw new Error(`dev-uploads tarball listed in the manifest is missing: ${abs}`);
942
1064
  }
943
1065
  return `file:${abs}`;
@@ -954,77 +1076,28 @@ var require_dist = __commonJS({
954
1076
  ...Object.keys(overrides).length > 0 ? { overrides } : {}
955
1077
  };
956
1078
  }
957
- // ── Rollback ──────────────────────────────────────────────────────────
1079
+ // ── Rollback (removed — single-copy has no N-1) ────────────────────────
1080
+ /**
1081
+ * Rollback is NOT supported in the single-copy model — there is no retained
1082
+ * N-1 copy to revert to (operator's accepted tradeoff). Recovery is manual:
1083
+ * re-apply a known-good version via {@link applyServerUpdate}, or reinstall
1084
+ * the image seed. Kept on the surface so the cap contract is unchanged.
1085
+ */
958
1086
  async rollbackServerUpdate() {
959
- if (this.inFlight !== "idle") {
960
- return {
961
- accepted: false,
962
- targetVersion: null,
963
- restarting: false,
964
- message: `Refused: another operation is in flight (${this.inFlight}).`
965
- };
966
- }
967
- const state = this.readState();
968
- if (state.pendingBoot !== null) {
969
- return {
970
- accepted: false,
971
- targetVersion: null,
972
- restarting: false,
973
- message: `Refused: version ${state.pendingBoot.version} is already staged and awaiting restart.`
974
- };
975
- }
976
- if (state.currentVersion === null || state.previousVersion === null) {
977
- return {
978
- accepted: false,
979
- targetVersion: state.previousVersion,
980
- restarting: false,
981
- message: "Refused: no previous version available to roll back to."
982
- };
983
- }
984
- const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
985
- const invalidReason = validateVersionDir(
986
- this.rootDir(),
987
- state.previousVersion,
988
- nodeMajor,
989
- this.spec
990
- );
991
- if (invalidReason !== null) {
992
- return {
993
- accepted: false,
994
- targetVersion: state.previousVersion,
995
- restarting: false,
996
- message: `Refused: previous version ${state.previousVersion} is not loadable (${invalidReason}).`
997
- };
998
- }
999
- writeServerRootState(this.rootDir(), {
1000
- ...state,
1001
- pendingBoot: {
1002
- version: state.previousVersion,
1003
- fromVersion: state.currentVersion,
1004
- requestedAtMs: this.now(),
1005
- bootAttempts: 0
1006
- }
1007
- });
1008
- this.logger.info("root package rollback requested \u2014 restarting", {
1009
- meta: { fromVersion: state.currentVersion, toVersion: state.previousVersion }
1010
- });
1011
- this.restartServerFn(
1012
- `${RESTART_REASON_PREFIX}: rollback to ${this.spec.packageName}@${state.previousVersion}`
1013
- );
1014
1087
  return {
1015
- accepted: true,
1016
- targetVersion: state.previousVersion,
1017
- restarting: true,
1018
- message: `Rolling back to ${this.spec.packageName}@${state.previousVersion} \u2014 restarting.`
1088
+ accepted: false,
1089
+ targetVersion: null,
1090
+ restarting: false,
1091
+ message: "Rollback is not supported in the single-copy model (no retained previous version). Recover by re-applying a known-good version via applyServerUpdate, or reinstall the image seed."
1019
1092
  };
1020
1093
  }
1021
1094
  // ── Plain restart ─────────────────────────────────────────────────────
1022
1095
  /**
1023
- * Plain process restart — no version change. Backs the `restartServer` cap
1024
- * method. Guards mirror apply/rollback: refuse while a stage is in flight
1025
- * (a concurrent npm install must not be interrupted) or while a version is
1026
- * already staged awaiting restart (a naive bounce would boot the OLD version
1027
- * and orphan the pending one — the operator should apply/rollback instead).
1096
+ * Plain process restart — no version change. Refuse while a stage is in
1097
+ * flight (a concurrent npm install must not be interrupted) or while a
1098
+ * version is already staged awaiting restart (a naive bounce would boot the
1099
+ * OLD version and orphan the pending swap the operator should apply
1100
+ * instead).
1028
1101
  */
1029
1102
  restartNode() {
1030
1103
  const runningVersion = this.runningVersion();
@@ -1036,13 +1109,12 @@ var require_dist = __commonJS({
1036
1109
  message: `Refused: another operation is in flight (${this.inFlight}).`
1037
1110
  };
1038
1111
  }
1039
- const state = this.readState();
1040
- if (state.pendingBoot !== null) {
1112
+ if (readPendingRootSwap(this.rootDir()) !== null) {
1041
1113
  return {
1042
1114
  accepted: false,
1043
1115
  targetVersion: runningVersion,
1044
1116
  restarting: false,
1045
- message: `Refused: version ${state.pendingBoot.version} is staged and awaiting restart \u2014 apply or roll it back instead of a plain restart.`
1117
+ message: "Refused: a version is staged and awaiting restart \u2014 apply it instead of a plain restart."
1046
1118
  };
1047
1119
  }
1048
1120
  this.logger.info("plain node restart requested", { meta: { runningVersion } });
@@ -1056,103 +1128,35 @@ var require_dist = __commonJS({
1056
1128
  }
1057
1129
  // ── Boot health confirmation ──────────────────────────────────────────
1058
1130
  /**
1059
- * Called by the post-boot path once the node is READY (hub: post-boot
1060
- * pipeline; agent: first acked `$hub.registerNode`). When THIS process is
1061
- * the probation boot of a pending version, promote it: `currentVersion`
1062
- * pending, `previousVersion` the version it replaced (N-1 retained for
1063
- * rollback), clear `pendingBoot` + `rolledBack`, and prune every version
1064
- * dir beyond {current, previous}. No-op otherwise.
1131
+ * Called by the post-boot path once the node is READY. In the single-copy
1132
+ * model there is no pending version to promote (the swap already happened in
1133
+ * the starter). This becomes a best-effort GC of orphaned transient dirs
1134
+ * (`.staging-*` / `.trash-*` left by a crash mid-swap) + the dev-uploads
1135
+ * sweep. Kept returning the legacy `{ promoted }` shape for the callers.
1065
1136
  */
1066
1137
  confirmBootHealthy() {
1067
- const state = readServerRootState(this.rootDir());
1068
- if (state === null || state.pendingBoot === null) return { promoted: null };
1069
- const activeVersion = this.env[this.envNames.activeVersion] ?? null;
1070
- if (activeVersion !== state.pendingBoot.version) {
1071
- return { promoted: null };
1072
- }
1073
- const promoted = {
1074
- ...state,
1075
- currentVersion: state.pendingBoot.version,
1076
- previousVersion: state.pendingBoot.fromVersion,
1077
- pendingBoot: null,
1078
- rolledBack: null
1079
- };
1080
- writeServerRootState(this.rootDir(), promoted);
1081
- this.pruneVersions(
1082
- [promoted.currentVersion, promoted.previousVersion].filter(
1083
- (v) => typeof v === "string"
1084
- )
1085
- );
1138
+ sweepTransientRootDirs(this.rootDir(), this.now(), STALE_TRANSIENT_MS);
1086
1139
  this.sweepDevUploads(DEV_UPLOADS_KEEP_COUNT);
1087
- this.logger.info("root package update confirmed healthy", {
1088
- meta: {
1089
- version: promoted.currentVersion,
1090
- previousVersion: promoted.previousVersion
1091
- }
1092
- });
1093
- return { promoted: promoted.currentVersion };
1094
- }
1095
- /**
1096
- * Remove version dirs under `versions/` not in `keep`.
1097
- *
1098
- * Transient dot-entries (`.staging-*` / `.evicted-*`) are NOT pruned unless
1099
- * older than {@link RootUpdateService.STALE_TRANSIENT_MS}: the API starts
1100
- * serving BEFORE the post-boot `confirmBootHealthy()` runs, so a concurrent
1101
- * `applyServerUpdate` can be mid-npm-install into a fresh `.staging-*` dir
1102
- * when this prune fires — deleting it would corrupt that install. Fresh
1103
- * transient dirs are owned by their in-flight operation (which cleans up on
1104
- * failure); the staleness sweep only collects dirs orphaned by a crash.
1105
- */
1106
- pruneVersions(keep) {
1107
- const vDir = versionsDir(this.rootDir());
1108
- let entries;
1109
- try {
1110
- entries = fs5.readdirSync(vDir);
1111
- } catch {
1112
- return;
1113
- }
1114
- for (const entry of entries) {
1115
- if (keep.includes(entry)) continue;
1116
- const full = path5.join(vDir, entry);
1117
- if (entry.startsWith(".")) {
1118
- try {
1119
- const ageMs = this.now() - fs5.statSync(full).mtimeMs;
1120
- if (ageMs < _RootUpdateService.STALE_TRANSIENT_MS) continue;
1121
- } catch {
1122
- continue;
1123
- }
1124
- }
1125
- try {
1126
- fs5.rmSync(full, { recursive: true, force: true });
1127
- this.logger.debug("pruned server-root version dir", { meta: { entry } });
1128
- } catch (err) {
1129
- this.logger.warn("failed to prune server-root version dir", {
1130
- meta: { entry, error: err instanceof Error ? err.message : String(err) }
1131
- });
1132
- }
1133
- }
1140
+ return { promoted: null };
1134
1141
  }
1135
1142
  /**
1136
1143
  * Promote-time sweep of `server-root/dev-uploads/`: keep only the
1137
1144
  * `keepCount` most recent entries (ordered by the self-describing
1138
1145
  * `-dev.<epochSeconds>` suffix; non-dev-shaped residue counts as oldest).
1139
- * Uses the same rename-aside graveyard pattern as the addon installer —
1140
- * a rename is a metadata op that succeeds even when a tarball is held
1141
- * open, and residue in the graveyard is re-swept on the next promote.
1142
1146
  */
1143
1147
  sweepDevUploads(keepCount) {
1144
1148
  const dir = devUploadsDir(this.rootDir());
1145
1149
  let entries;
1146
1150
  try {
1147
- entries = fs5.readdirSync(dir);
1151
+ entries = fs6.readdirSync(dir);
1148
1152
  } catch {
1149
1153
  return;
1150
1154
  }
1151
- const graveyard = path5.join(dir, ".sweeping");
1155
+ const graveyard = path6.join(dir, ".sweeping");
1152
1156
  try {
1153
- for (const residue of fs5.readdirSync(graveyard)) {
1157
+ for (const residue of fs6.readdirSync(graveyard)) {
1154
1158
  try {
1155
- fs5.rmSync(path5.join(graveyard, residue), { recursive: true, force: true });
1159
+ fs6.rmSync(path6.join(graveyard, residue), { recursive: true, force: true });
1156
1160
  } catch {
1157
1161
  }
1158
1162
  }
@@ -1164,11 +1168,11 @@ var require_dist = __commonJS({
1164
1168
  );
1165
1169
  const doomed = byNewestFirst.slice(keepCount);
1166
1170
  if (doomed.length === 0) return;
1167
- fs5.mkdirSync(graveyard, { recursive: true });
1171
+ fs6.mkdirSync(graveyard, { recursive: true });
1168
1172
  for (const entry of doomed) {
1169
- const aside = path5.join(graveyard, `${entry}-${this.now()}`);
1173
+ const aside = path6.join(graveyard, `${entry}-${this.now()}`);
1170
1174
  try {
1171
- fs5.renameSync(path5.join(dir, entry), aside);
1175
+ fs6.renameSync(path6.join(dir, entry), aside);
1172
1176
  } catch (err) {
1173
1177
  this.logger.warn("failed to move dev-uploads entry aside for sweep", {
1174
1178
  meta: { entry, error: err instanceof Error ? err.message : String(err) }
@@ -1176,14 +1180,12 @@ var require_dist = __commonJS({
1176
1180
  continue;
1177
1181
  }
1178
1182
  try {
1179
- fs5.rmSync(aside, { recursive: true, force: true });
1183
+ fs6.rmSync(aside, { recursive: true, force: true });
1180
1184
  this.logger.debug("swept dev-uploads entry", { meta: { entry } });
1181
1185
  } catch {
1182
1186
  }
1183
1187
  }
1184
1188
  }
1185
- /** Transient (.staging- / .evicted-) dirs younger than this are never pruned. */
1186
- static STALE_TRANSIENT_MS = 24 * 60 * 60 * 1e3;
1187
1189
  };
1188
1190
  }
1189
1191
  });
@@ -1193,4 +1195,4 @@ export {
1193
1195
  __toESM,
1194
1196
  require_dist
1195
1197
  };
1196
- //# sourceMappingURL=chunk-KPUXW5YX.mjs.map
1198
+ //# sourceMappingURL=chunk-ADZNLSIO.mjs.map