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