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