@camstack/server 1.1.70 → 1.1.72

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.
@@ -74,31 +74,41 @@ var require_dist = __commonJS({
74
74
  var src_exports = {};
75
75
  __export2(src_exports, {
76
76
  AGENT_ROOT_SPEC: /* @__PURE__ */ __name(() => AGENT_ROOT_SPEC, "AGENT_ROOT_SPEC"),
77
+ CURRENT_DIRNAME: /* @__PURE__ */ __name(() => CURRENT_DIRNAME, "CURRENT_DIRNAME"),
77
78
  DEV_UPLOADS_DIRNAME: /* @__PURE__ */ __name(() => DEV_UPLOADS_DIRNAME, "DEV_UPLOADS_DIRNAME"),
78
79
  DEV_UPLOADS_KEEP_COUNT: /* @__PURE__ */ __name(() => DEV_UPLOADS_KEEP_COUNT, "DEV_UPLOADS_KEEP_COUNT"),
79
80
  DEV_UPLOAD_MANIFEST_FILE: /* @__PURE__ */ __name(() => DEV_UPLOAD_MANIFEST_FILE, "DEV_UPLOAD_MANIFEST_FILE"),
80
81
  HOST_EXTERNAL_SPECIFIERS: /* @__PURE__ */ __name(() => HOST_EXTERNAL_SPECIFIERS, "HOST_EXTERNAL_SPECIFIERS"),
81
82
  HUB_ROOT_SPEC: /* @__PURE__ */ __name(() => HUB_ROOT_SPEC2, "HUB_ROOT_SPEC"),
83
+ PENDING_ROOT_SWAP_FILE: /* @__PURE__ */ __name(() => PENDING_ROOT_SWAP_FILE, "PENDING_ROOT_SWAP_FILE"),
84
+ REQUIRED_NATIVE_PACKAGES: /* @__PURE__ */ __name(() => REQUIRED_NATIVE_PACKAGES, "REQUIRED_NATIVE_PACKAGES"),
82
85
  RESTART_INTENT_FILE: /* @__PURE__ */ __name(() => RESTART_INTENT_FILE, "RESTART_INTENT_FILE"),
83
86
  RootUpdateService: /* @__PURE__ */ __name(() => RootUpdateService, "RootUpdateService"),
84
87
  SERVER_ROOT_DIRNAME: /* @__PURE__ */ __name(() => SERVER_ROOT_DIRNAME, "SERVER_ROOT_DIRNAME"),
85
88
  SERVER_ROOT_STATE_FILE: /* @__PURE__ */ __name(() => SERVER_ROOT_STATE_FILE, "SERVER_ROOT_STATE_FILE"),
89
+ applyPendingRootSwap: /* @__PURE__ */ __name(() => applyPendingRootSwap, "applyPendingRootSwap"),
90
+ clearPendingRootSwap: /* @__PURE__ */ __name(() => clearPendingRootSwap, "clearPendingRootSwap"),
86
91
  clearRestartIntentMarker: /* @__PURE__ */ __name(() => clearRestartIntentMarker, "clearRestartIntentMarker"),
87
92
  compareSemver: /* @__PURE__ */ __name(() => compareSemver, "compareSemver"),
93
+ currentDir: /* @__PURE__ */ __name(() => currentDir, "currentDir"),
94
+ currentEntryPath: /* @__PURE__ */ __name(() => currentEntryPath, "currentEntryPath"),
88
95
  detectWorkspaceRoot: /* @__PURE__ */ __name(() => detectWorkspaceRoot, "detectWorkspaceRoot"),
89
96
  devChannelEpoch: /* @__PURE__ */ __name(() => devChannelEpoch, "devChannelEpoch"),
90
97
  devUploadManifestPath: /* @__PURE__ */ __name(() => devUploadManifestPath, "devUploadManifestPath"),
91
98
  devUploadVersionDir: /* @__PURE__ */ __name(() => devUploadVersionDir, "devUploadVersionDir"),
92
99
  devUploadsDir: /* @__PURE__ */ __name(() => devUploadsDir, "devUploadsDir"),
93
100
  emptyServerRootState: /* @__PURE__ */ __name(() => emptyServerRootState, "emptyServerRootState"),
101
+ findMissingNativePrebuilds: /* @__PURE__ */ __name(() => findMissingNativePrebuilds, "findMissingNativePrebuilds"),
94
102
  isDevChannelVersion: /* @__PURE__ */ __name(() => isDevChannelVersion, "isDevChannelVersion"),
95
103
  isHostExternal: /* @__PURE__ */ __name(() => isHostExternal, "isHostExternal"),
96
104
  isServerRootState: /* @__PURE__ */ __name(() => isServerRootState, "isServerRootState"),
105
+ migrateToSingleCopy: /* @__PURE__ */ __name(() => migrateToSingleCopy, "migrateToSingleCopy"),
97
106
  minNodeMajorOf: /* @__PURE__ */ __name(() => minNodeMajorOf, "minNodeMajorOf"),
107
+ pendingRootSwapPath: /* @__PURE__ */ __name(() => pendingRootSwapPath, "pendingRootSwapPath"),
98
108
  planBoot: /* @__PURE__ */ __name(() => planBoot, "planBoot"),
99
109
  readDevUploadManifest: /* @__PURE__ */ __name(() => readDevUploadManifest, "readDevUploadManifest"),
110
+ readPendingRootSwap: /* @__PURE__ */ __name(() => readPendingRootSwap, "readPendingRootSwap"),
100
111
  readRestartIntentMarker: /* @__PURE__ */ __name(() => readRestartIntentMarker, "readRestartIntentMarker"),
101
- readSeedVersion: /* @__PURE__ */ __name(() => readSeedVersion, "readSeedVersion"),
102
112
  readServerRootState: /* @__PURE__ */ __name(() => readServerRootState, "readServerRootState"),
103
113
  registerActiveRootResolver: /* @__PURE__ */ __name(() => registerActiveRootResolver, "registerActiveRootResolver"),
104
114
  restartIntentMarkerPath: /* @__PURE__ */ __name(() => restartIntentMarkerPath, "restartIntentMarkerPath"),
@@ -106,11 +116,15 @@ var require_dist = __commonJS({
106
116
  rootPackageDir: /* @__PURE__ */ __name(() => rootPackageDir2, "rootPackageDir"),
107
117
  runNodeStarter: /* @__PURE__ */ __name(() => runNodeStarter, "runNodeStarter"),
108
118
  serverRootDir: /* @__PURE__ */ __name(() => serverRootDir, "serverRootDir"),
119
+ stagingDirPath: /* @__PURE__ */ __name(() => stagingDirPath, "stagingDirPath"),
109
120
  stateFilePath: /* @__PURE__ */ __name(() => stateFilePath, "stateFilePath"),
121
+ sweepTransientRootDirs: /* @__PURE__ */ __name(() => sweepTransientRootDirs, "sweepTransientRootDirs"),
122
+ validateClosureDir: /* @__PURE__ */ __name(() => validateClosureDir, "validateClosureDir"),
110
123
  validateVersionDir: /* @__PURE__ */ __name(() => validateVersionDir, "validateVersionDir"),
111
124
  versionDir: /* @__PURE__ */ __name(() => versionDir, "versionDir"),
112
125
  versionsDir: /* @__PURE__ */ __name(() => versionsDir, "versionsDir"),
113
126
  writeDevUploadManifest: /* @__PURE__ */ __name(() => writeDevUploadManifest, "writeDevUploadManifest"),
127
+ writePendingRootSwap: /* @__PURE__ */ __name(() => writePendingRootSwap, "writePendingRootSwap"),
114
128
  writeRestartIntentMarker: /* @__PURE__ */ __name(() => writeRestartIntentMarker, "writeRestartIntentMarker"),
115
129
  writeServerRootState: /* @__PURE__ */ __name(() => writeServerRootState, "writeServerRootState")
116
130
  });
@@ -316,13 +330,12 @@ var require_dist = __commonJS({
316
330
  return Number.isNaN(major) ? null : major;
317
331
  }
318
332
  __name(minNodeMajorOf, "minNodeMajorOf");
319
- function validateVersionDir(rootDir, version, nodeMajor, spec) {
320
- const vDir = versionDir(rootDir, version);
321
- const entry = rootEntryPath2(vDir, spec);
333
+ function validateClosureDir(closureDir, nodeMajor, spec, expectedVersion) {
334
+ const entry = rootEntryPath2(closureDir, spec);
322
335
  if (!fs2.existsSync(entry)) {
323
336
  return `root entry missing: ${entry}`;
324
337
  }
325
- const pkgJsonPath = path2.join(rootPackageDir2(vDir, spec), "package.json");
338
+ const pkgJsonPath = path2.join(rootPackageDir2(closureDir, spec), "package.json");
326
339
  let pkg;
327
340
  try {
328
341
  const raw = JSON.parse(fs2.readFileSync(pkgJsonPath, "utf-8"));
@@ -336,8 +349,8 @@ var require_dist = __commonJS({
336
349
  if (pkg["name"] !== spec.packageName) {
337
350
  return `package name mismatch: expected ${spec.packageName}, got ${String(pkg["name"])}`;
338
351
  }
339
- if (pkg["version"] !== version) {
340
- return `package version mismatch: dir says ${version}, package.json says ${String(pkg["version"])}`;
352
+ if (expectedVersion !== void 0 && pkg["version"] !== expectedVersion) {
353
+ return `package version mismatch: expected ${expectedVersion}, package.json says ${String(pkg["version"])}`;
341
354
  }
342
355
  const engines = pkg["engines"];
343
356
  if (typeof engines === "object" && engines !== null) {
@@ -351,7 +364,13 @@ var require_dist = __commonJS({
351
364
  }
352
365
  return null;
353
366
  }
367
+ __name(validateClosureDir, "validateClosureDir");
368
+ function validateVersionDir(rootDir, version, nodeMajor, spec) {
369
+ return validateClosureDir(versionDir(rootDir, version), nodeMajor, spec, version);
370
+ }
354
371
  __name(validateVersionDir, "validateVersionDir");
372
+ var fs3 = __toESM2(require("fs"));
373
+ var path3 = __toESM2(require("path"));
355
374
  function parse(version) {
356
375
  const dashIdx = version.indexOf("-");
357
376
  const base = dashIdx === -1 ? version : version.slice(0, dashIdx);
@@ -384,142 +403,344 @@ var require_dist = __commonJS({
384
403
  return 0;
385
404
  }
386
405
  __name(compareSemver, "compareSemver");
387
- function planBoot(state, isValidVersion, now, seedVersion = null) {
388
- if (state === null) {
389
- return {
390
- kind: "baked",
391
- reason: "no server-root state",
392
- stateToWrite: null
393
- };
406
+ var CURRENT_DIRNAME = "current";
407
+ var PENDING_ROOT_SWAP_FILE = ".pending-root-swap.json";
408
+ var LEGACY_FRAMEWORK_MARKERS = [
409
+ ".pending-framework-swap.json",
410
+ ".framework-swap-confirm.json"
411
+ ];
412
+ function currentDir(rootDir) {
413
+ return path3.join(rootDir, CURRENT_DIRNAME);
414
+ }
415
+ __name(currentDir, "currentDir");
416
+ function currentEntryPath(rootDir, spec) {
417
+ return rootEntryPath2(currentDir(rootDir), spec);
418
+ }
419
+ __name(currentEntryPath, "currentEntryPath");
420
+ function stagingDirPath(rootDir, target, pid, now) {
421
+ return path3.join(rootDir, `.staging-${target}-${pid}-${now}`);
422
+ }
423
+ __name(stagingDirPath, "stagingDirPath");
424
+ function pendingRootSwapPath(rootDir) {
425
+ return path3.join(rootDir, PENDING_ROOT_SWAP_FILE);
426
+ }
427
+ __name(pendingRootSwapPath, "pendingRootSwapPath");
428
+ function isPendingRootSwap(v) {
429
+ if (typeof v !== "object" || v === null) return false;
430
+ const m = v;
431
+ return m["schemaVersion"] === 1 && typeof m["version"] === "string" && typeof m["stagingPath"] === "string" && typeof m["requestedAtMs"] === "number";
432
+ }
433
+ __name(isPendingRootSwap, "isPendingRootSwap");
434
+ function readPendingRootSwap(rootDir) {
435
+ try {
436
+ const raw = JSON.parse(fs3.readFileSync(pendingRootSwapPath(rootDir), "utf-8"));
437
+ return isPendingRootSwap(raw) ? raw : null;
438
+ } catch {
439
+ return null;
394
440
  }
395
- let next = state;
396
- let changed = false;
397
- if (next.pendingBoot !== null) {
398
- const pending = next.pendingBoot;
399
- if (pending.bootAttempts >= 1) {
400
- next = {
401
- ...next,
402
- pendingBoot: null,
403
- rolledBack: {
404
- fromVersion: pending.version,
405
- toVersion: next.currentVersion,
406
- atMs: now(),
407
- reason: "probation boot did not reach ready \u2014 rolled back"
408
- }
409
- };
410
- changed = true;
411
- } else if (!isValidVersion(pending.version)) {
412
- next = {
413
- ...next,
414
- pendingBoot: null,
415
- rolledBack: {
416
- fromVersion: pending.version,
417
- toVersion: next.currentVersion,
418
- atMs: now(),
419
- reason: "staged version failed validation"
420
- }
421
- };
422
- changed = true;
423
- } else {
441
+ }
442
+ __name(readPendingRootSwap, "readPendingRootSwap");
443
+ function writePendingRootSwap(rootDir, marker) {
444
+ fs3.mkdirSync(rootDir, {
445
+ recursive: true
446
+ });
447
+ const target = pendingRootSwapPath(rootDir);
448
+ const tmp = `${target}.tmp`;
449
+ fs3.writeFileSync(tmp, JSON.stringify(marker, null, 2), "utf-8");
450
+ fs3.renameSync(tmp, target);
451
+ }
452
+ __name(writePendingRootSwap, "writePendingRootSwap");
453
+ function clearPendingRootSwap(rootDir) {
454
+ try {
455
+ fs3.rmSync(pendingRootSwapPath(rootDir), {
456
+ force: true
457
+ });
458
+ } catch {
459
+ }
460
+ }
461
+ __name(clearPendingRootSwap, "clearPendingRootSwap");
462
+ var REQUIRED_NATIVE_PACKAGES = [
463
+ "better-sqlite3",
464
+ "sharp",
465
+ "node-av"
466
+ ];
467
+ var NATIVE_SEARCH_DIRS = {
468
+ "better-sqlite3": [
469
+ "better-sqlite3"
470
+ ],
471
+ sharp: [
472
+ "sharp",
473
+ "@img"
474
+ ],
475
+ "node-av": [
476
+ "node-av",
477
+ "@seydx"
478
+ ]
479
+ };
480
+ function hasDotNode(dir, maxDepth) {
481
+ let entries;
482
+ try {
483
+ entries = fs3.readdirSync(dir, {
484
+ withFileTypes: true
485
+ });
486
+ } catch {
487
+ return false;
488
+ }
489
+ for (const entry of entries) {
490
+ if (entry.isFile() && entry.name.endsWith(".node")) return true;
491
+ }
492
+ if (maxDepth <= 0) return false;
493
+ for (const entry of entries) {
494
+ if (entry.isDirectory() && hasDotNode(path3.join(dir, entry.name), maxDepth - 1)) return true;
495
+ }
496
+ return false;
497
+ }
498
+ __name(hasDotNode, "hasDotNode");
499
+ function findMissingNativePrebuilds(closureDir) {
500
+ const nm = path3.join(closureDir, "node_modules");
501
+ const missing = [];
502
+ for (const pkg of REQUIRED_NATIVE_PACKAGES) {
503
+ const searchDirs = NATIVE_SEARCH_DIRS[pkg] ?? [
504
+ pkg
505
+ ];
506
+ const found = searchDirs.some((rel) => hasDotNode(path3.join(nm, ...rel.split("/")), 4));
507
+ if (!found) missing.push(pkg);
508
+ }
509
+ return missing;
510
+ }
511
+ __name(findMissingNativePrebuilds, "findMissingNativePrebuilds");
512
+ function rmrf(target) {
513
+ try {
514
+ fs3.rmSync(target, {
515
+ recursive: true,
516
+ force: true
517
+ });
518
+ } catch {
519
+ }
520
+ }
521
+ __name(rmrf, "rmrf");
522
+ function errMsg(err) {
523
+ return err instanceof Error ? err.message : String(err);
524
+ }
525
+ __name(errMsg, "errMsg");
526
+ function applyPendingRootSwap(rootDir, spec, nodeMajor, now, log) {
527
+ const marker = readPendingRootSwap(rootDir);
528
+ if (marker === null) return {
529
+ applied: false,
530
+ version: null,
531
+ reason: "no pending swap"
532
+ };
533
+ const staging = marker.stagingPath;
534
+ const dest = currentDir(rootDir);
535
+ const invalid = validateClosureDir(staging, nodeMajor, spec, marker.version);
536
+ if (invalid !== null) {
537
+ if (!fs3.existsSync(staging) && validateClosureDir(dest, nodeMajor, spec, marker.version) === null) {
538
+ clearPendingRootSwap(rootDir);
539
+ log(`[single-copy] pending swap ${marker.version} already applied \u2014 cleared marker`);
424
540
  return {
425
- kind: "data-root",
426
- version: pending.version,
427
- probation: true,
428
- stateToWrite: {
429
- ...next,
430
- pendingBoot: {
431
- ...pending,
432
- bootAttempts: pending.bootAttempts + 1
433
- }
434
- }
541
+ applied: true,
542
+ version: marker.version,
543
+ reason: "already applied"
435
544
  };
436
545
  }
546
+ log(`[single-copy] staged swap ${marker.version} invalid \u2014 discarding (${invalid})`);
547
+ rmrf(staging);
548
+ clearPendingRootSwap(rootDir);
549
+ return {
550
+ applied: false,
551
+ version: null,
552
+ reason: invalid
553
+ };
437
554
  }
438
- if (next.currentVersion !== null) {
439
- if (isValidVersion(next.currentVersion)) {
440
- if (!changed && seedVersion !== null && compareSemver(seedVersion, next.currentVersion) > 0) {
441
- return {
442
- kind: "baked",
443
- reason: `baked seed ${seedVersion} is newer than active data-root ${next.currentVersion} \u2014 adopting the image seed`,
444
- stateToWrite: {
445
- ...next,
446
- currentVersion: null,
447
- previousVersion: null,
448
- pendingBoot: null,
449
- rolledBack: null
450
- }
451
- };
555
+ const missing = findMissingNativePrebuilds(staging);
556
+ if (missing.length > 0) {
557
+ log(`[single-copy] staged swap ${marker.version} missing native prebuilds: ${missing.join(", ")} \u2014 discarding`);
558
+ rmrf(staging);
559
+ clearPendingRootSwap(rootDir);
560
+ return {
561
+ applied: false,
562
+ version: null,
563
+ reason: `missing native prebuilds: ${missing.join(", ")}`
564
+ };
565
+ }
566
+ let trash = null;
567
+ try {
568
+ if (fs3.existsSync(dest)) {
569
+ trash = path3.join(rootDir, `.trash-${now()}-${process.pid}`);
570
+ fs3.renameSync(dest, trash);
571
+ }
572
+ try {
573
+ fs3.renameSync(staging, dest);
574
+ } catch (err) {
575
+ if (trash !== null && !fs3.existsSync(dest)) {
576
+ try {
577
+ fs3.renameSync(trash, dest);
578
+ } catch {
579
+ }
452
580
  }
453
- return {
454
- kind: "data-root",
455
- version: next.currentVersion,
456
- probation: false,
457
- stateToWrite: changed ? next : null
458
- };
581
+ throw err;
582
+ }
583
+ } catch (err) {
584
+ log(`[single-copy] swap of ${marker.version} FAILED \u2014 current preserved (${errMsg(err)})`);
585
+ return {
586
+ applied: false,
587
+ version: null,
588
+ reason: errMsg(err)
589
+ };
590
+ }
591
+ if (trash !== null) rmrf(trash);
592
+ clearPendingRootSwap(rootDir);
593
+ log(`[single-copy] applied staged root swap \u2192 ${marker.version}`);
594
+ return {
595
+ applied: true,
596
+ version: marker.version,
597
+ reason: "applied"
598
+ };
599
+ }
600
+ __name(applyPendingRootSwap, "applyPendingRootSwap");
601
+ function readClosureVersion(closureDir, spec) {
602
+ try {
603
+ const raw = JSON.parse(fs3.readFileSync(path3.join(rootPackageDir2(closureDir, spec), "package.json"), "utf-8"));
604
+ if (typeof raw === "object" && raw !== null) {
605
+ const version = raw["version"];
606
+ if (typeof version === "string") return version;
459
607
  }
460
- const broken = next.currentVersion;
461
- if (next.previousVersion !== null && isValidVersion(next.previousVersion)) {
462
- const fallback = next.previousVersion;
608
+ } catch {
609
+ }
610
+ return null;
611
+ }
612
+ __name(readClosureVersion, "readClosureVersion");
613
+ function isAdoptablePendingClosure(rootDir, version, nodeMajor, spec) {
614
+ const dir = versionDir(rootDir, version);
615
+ return validateClosureDir(dir, nodeMajor, spec, version) === null && findMissingNativePrebuilds(dir).length === 0;
616
+ }
617
+ __name(isAdoptablePendingClosure, "isAdoptablePendingClosure");
618
+ function migrateToSingleCopy(rootDir, spec, nodeMajor, now, log) {
619
+ const dest = currentDir(rootDir);
620
+ const legacyState = readServerRootState(rootDir);
621
+ const pendingVersion = legacyState?.pendingBoot?.version ?? null;
622
+ const pendingAdoptable = pendingVersion !== null && isAdoptablePendingClosure(rootDir, pendingVersion, nodeMajor, spec);
623
+ const currentValid = fs3.existsSync(dest) && validateClosureDir(dest, nodeMajor, spec) === null;
624
+ if (currentValid) {
625
+ const currentVersion = readClosureVersion(dest, spec);
626
+ const pendingSupersedes = pendingAdoptable && pendingVersion !== null && (currentVersion === null || compareSemver(pendingVersion, currentVersion) > 0);
627
+ if (!pendingSupersedes) {
463
628
  return {
464
- kind: "data-root",
465
- version: fallback,
466
- probation: false,
467
- stateToWrite: {
468
- ...next,
469
- currentVersion: fallback,
470
- previousVersion: null,
471
- rolledBack: {
472
- fromVersion: broken,
473
- toVersion: fallback,
474
- atMs: now(),
475
- reason: "active version failed validation"
476
- }
477
- }
629
+ migrated: false,
630
+ version: null,
631
+ reason: "current already present"
478
632
  };
479
633
  }
634
+ log(`[single-copy] in-flight pending ${pendingVersion} supersedes older current ${currentVersion ?? "?"}`);
635
+ }
636
+ const candidates = [];
637
+ if (pendingAdoptable && pendingVersion !== null) candidates.push(pendingVersion);
638
+ if (legacyState !== null) {
639
+ for (const v of [
640
+ legacyState.currentVersion,
641
+ legacyState.pendingBoot?.version ?? null,
642
+ legacyState.previousVersion
643
+ ]) {
644
+ if (typeof v === "string" && !candidates.includes(v)) candidates.push(v);
645
+ }
646
+ }
647
+ try {
648
+ for (const entry of fs3.readdirSync(versionsDir(rootDir))) {
649
+ if (!entry.startsWith(".") && !candidates.includes(entry)) candidates.push(entry);
650
+ }
651
+ } catch {
652
+ }
653
+ const source = candidates.find((v) => validateClosureDir(versionDir(rootDir, v), nodeMajor, spec, v) === null);
654
+ if (source === void 0) {
655
+ return {
656
+ migrated: false,
657
+ version: null,
658
+ reason: "no valid legacy versions/<X> to adopt"
659
+ };
660
+ }
661
+ if (fs3.existsSync(dest)) {
662
+ const aside = path3.join(rootDir, `.trash-${now()}-${process.pid}-stale-current`);
663
+ try {
664
+ fs3.renameSync(dest, aside);
665
+ } catch {
666
+ rmrf(dest);
667
+ }
668
+ }
669
+ try {
670
+ fs3.renameSync(versionDir(rootDir, source), dest);
671
+ } catch (err) {
672
+ log(`[single-copy] migration rename of versions/${source} failed (${errMsg(err)})`);
480
673
  return {
481
- kind: "baked",
482
- reason: `active version ${broken} failed validation and no valid previous version exists`,
483
- stateToWrite: {
484
- ...next,
485
- currentVersion: null,
486
- rolledBack: {
487
- fromVersion: broken,
488
- toVersion: null,
489
- atMs: now(),
490
- reason: "active version failed validation"
491
- }
492
- }
674
+ migrated: false,
675
+ version: null,
676
+ reason: errMsg(err)
677
+ };
678
+ }
679
+ rmrf(versionsDir(rootDir));
680
+ rmrf(stateFilePath(rootDir));
681
+ const dataDir = path3.dirname(rootDir);
682
+ for (const marker of LEGACY_FRAMEWORK_MARKERS) rmrf(path3.join(dataDir, marker));
683
+ log(`[single-copy] migrated legacy versions/${source} \u2192 current`);
684
+ return {
685
+ migrated: true,
686
+ version: source,
687
+ reason: "adopted legacy version"
688
+ };
689
+ }
690
+ __name(migrateToSingleCopy, "migrateToSingleCopy");
691
+ function sweepTransientRootDirs(rootDir, now, staleMs) {
692
+ let entries;
693
+ try {
694
+ entries = fs3.readdirSync(rootDir);
695
+ } catch {
696
+ return;
697
+ }
698
+ for (const entry of entries) {
699
+ if (!entry.startsWith(".staging-") && !entry.startsWith(".trash-")) continue;
700
+ const full = path3.join(rootDir, entry);
701
+ try {
702
+ if (now - fs3.statSync(full).mtimeMs < staleMs) continue;
703
+ } catch {
704
+ continue;
705
+ }
706
+ rmrf(full);
707
+ }
708
+ }
709
+ __name(sweepTransientRootDirs, "sweepTransientRootDirs");
710
+ function planBoot(currentValid) {
711
+ if (currentValid) {
712
+ return {
713
+ kind: "current",
714
+ reason: "single-copy closure present and valid"
493
715
  };
494
716
  }
495
717
  return {
496
718
  kind: "baked",
497
- reason: "no active data-root version",
498
- stateToWrite: changed ? next : null
719
+ reason: "no valid single-copy closure \u2014 booting the baked seed"
499
720
  };
500
721
  }
501
722
  __name(planBoot, "planBoot");
502
- var fs3 = __toESM2(require("fs"));
503
- var path3 = __toESM2(require("path"));
723
+ var fs4 = __toESM2(require("fs"));
724
+ var path4 = __toESM2(require("path"));
504
725
  function detectWorkspaceRoot(fromDir) {
505
- let dir = path3.resolve(fromDir);
726
+ let dir = path4.resolve(fromDir);
506
727
  for (; ; ) {
507
- const pkgPath = path3.join(dir, "package.json");
728
+ const pkgPath = path4.join(dir, "package.json");
508
729
  try {
509
- const raw = JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
730
+ const raw = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
510
731
  if (typeof raw === "object" && raw !== null && raw["workspaces"] !== void 0) {
511
732
  return dir;
512
733
  }
513
734
  } catch {
514
735
  }
515
- const parent = path3.dirname(dir);
736
+ const parent = path4.dirname(dir);
516
737
  if (parent === dir) return null;
517
738
  dir = parent;
518
739
  }
519
740
  }
520
741
  __name(detectWorkspaceRoot, "detectWorkspaceRoot");
521
- var fs4 = __toESM2(require("fs"));
522
- var path4 = __toESM2(require("path"));
742
+ var fs5 = __toESM2(require("fs"));
743
+ var path5 = __toESM2(require("path"));
523
744
  var import_node_url = require("url");
524
745
  var HOST_EXTERNAL_SPECIFIERS = [
525
746
  "@camstack/system",
@@ -543,7 +764,7 @@ var require_dist = __commonJS({
543
764
  console.warn("[starter] module.registerHooks unavailable (Node < 22.15) \u2014 host-external redirect skipped");
544
765
  return;
545
766
  }
546
- const anchorURL = (0, import_node_url.pathToFileURL)(path4.join(activeRootDir, "node_modules", "__camstack_starter_anchor__.js")).href;
767
+ const anchorURL = (0, import_node_url.pathToFileURL)(path5.join(activeRootDir, "node_modules", "__camstack_starter_anchor__.js")).href;
547
768
  const hooks = {
548
769
  resolve: /* @__PURE__ */ __name((specifier, context, nextResolve) => {
549
770
  if (isHostExternal(specifier)) {
@@ -561,24 +782,23 @@ var require_dist = __commonJS({
561
782
  registerHooks(hooks);
562
783
  }
563
784
  __name(registerActiveRootResolver, "registerActiveRootResolver");
564
- function readSeedVersion(seedEntry) {
785
+ function readClosureVersion2(closureDir, spec) {
565
786
  try {
566
- const pkgJsonPath = path4.join(path4.dirname(seedEntry), "..", "package.json");
567
- const raw = JSON.parse(fs4.readFileSync(pkgJsonPath, "utf-8"));
787
+ const raw = JSON.parse(fs5.readFileSync(path5.join(rootPackageDir2(closureDir, spec), "package.json"), "utf-8"));
568
788
  if (typeof raw === "object" && raw !== null) {
569
789
  const version = raw["version"];
570
790
  if (typeof version === "string") return version;
571
791
  }
572
- return null;
573
792
  } catch {
574
- return null;
575
793
  }
794
+ return null;
576
795
  }
577
- __name(readSeedVersion, "readSeedVersion");
796
+ __name(readClosureVersion2, "readClosureVersion2");
578
797
  function runNodeStarter(options) {
579
798
  const env = options.env ?? process.env;
580
799
  const now = options.now ?? Date.now;
581
800
  const registerResolver = options.registerResolver ?? registerActiveRootResolver;
801
+ const nodeMajor = options.nodeMajor ?? Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
582
802
  const disabled = env[options.envNames.killSwitch] === "off";
583
803
  const workspaceRoot = detectWorkspaceRoot(options.starterDir);
584
804
  if (workspaceRoot !== null) {
@@ -594,36 +814,24 @@ var require_dist = __commonJS({
594
814
  return;
595
815
  }
596
816
  const rootDir = serverRootDir(options.dataDir);
597
- const state = readServerRootState(rootDir);
598
- const nodeMajor = options.nodeMajor ?? Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
599
- const isValidVersion = /* @__PURE__ */ __name((version) => {
600
- const reason = validateVersionDir(rootDir, version, nodeMajor, options.spec);
601
- if (reason !== null) console.warn(`[starter] version ${version} invalid: ${reason}`);
602
- return reason === null;
603
- }, "isValidVersion");
604
- const seedVersion = readSeedVersion(options.seedEntry);
605
- const plan = planBoot(state, isValidVersion, now, seedVersion);
606
- if (plan.stateToWrite !== null) {
607
- try {
608
- writeServerRootState(rootDir, plan.stateToWrite);
609
- } catch (err) {
610
- console.error("[starter] FAILED to persist server-root state \u2014 booting baked seed:", err);
611
- env[options.envNames.bootMode] = "baked";
612
- options.loadEntry(options.seedEntry);
613
- return;
614
- }
615
- if (plan.stateToWrite.rolledBack !== null && (state?.rolledBack ?? null) !== plan.stateToWrite.rolledBack) {
616
- const rb = plan.stateToWrite.rolledBack;
617
- console.warn(`[starter] ROLLED BACK ${rb.fromVersion} -> ${rb.toVersion ?? "baked seed"}: ${rb.reason}`);
618
- }
817
+ const swap = applyPendingRootSwap(rootDir, options.spec, nodeMajor, now, (m) => console.log(m));
818
+ if (swap.applied) console.log(`[starter] applied staged update \u2192 ${swap.version ?? "?"}`);
819
+ else if (swap.reason !== "no pending swap") console.warn(`[starter] staged update NOT applied: ${swap.reason}`);
820
+ const migration = migrateToSingleCopy(rootDir, options.spec, nodeMajor, now, (m) => console.log(m));
821
+ if (migration.migrated) console.log(`[starter] migrated legacy layout \u2192 single copy (${migration.version ?? "?"})`);
822
+ const activeRootDir = currentDir(rootDir);
823
+ const invalid = validateClosureDir(activeRootDir, nodeMajor, options.spec);
824
+ if (invalid !== null && fs5.existsSync(activeRootDir)) {
825
+ console.warn(`[starter] single copy invalid: ${invalid}`);
619
826
  }
620
- if (plan.kind === "data-root") {
621
- const activeRootDir = versionDir(rootDir, plan.version);
622
- const entry = rootEntryPath2(activeRootDir, options.spec);
623
- console.log(`[starter] booting ${options.spec.packageName}@${plan.version} from ${activeRootDir}` + (plan.probation ? " (probation boot \u2014 health-check armed)" : ""));
827
+ const plan = planBoot(invalid === null);
828
+ if (plan.kind === "current") {
829
+ const version = readClosureVersion2(activeRootDir, options.spec);
830
+ const entry = currentEntryPath(rootDir, options.spec);
831
+ console.log(`[starter] booting ${options.spec.packageName}@${version ?? "?"} from ${activeRootDir}`);
624
832
  env[options.envNames.bootMode] = "data-root";
625
833
  env[options.envNames.activeRoot] = activeRootDir;
626
- env[options.envNames.activeVersion] = plan.version;
834
+ if (version !== null) env[options.envNames.activeVersion] = version;
627
835
  registerResolver(activeRootDir);
628
836
  options.loadEntry(entry);
629
837
  return;
@@ -634,8 +842,8 @@ var require_dist = __commonJS({
634
842
  }
635
843
  __name(runNodeStarter, "runNodeStarter");
636
844
  var import_node_child_process = require("child_process");
637
- var fs5 = __toESM2(require("fs"));
638
- var path5 = __toESM2(require("path"));
845
+ var fs6 = __toESM2(require("fs"));
846
+ var path6 = __toESM2(require("path"));
639
847
  var import_node_util = require("util");
640
848
  var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
641
849
  function buildNpmRegistryArgs(registry) {
@@ -650,9 +858,10 @@ var require_dist = __commonJS({
650
858
  var NPM_VIEW_TIMEOUT_MS = 2e4;
651
859
  var NPM_INSTALL_TIMEOUT_MS = 15 * 6e4;
652
860
  var RESTART_REASON_PREFIX = "server-update";
861
+ var STALE_TRANSIENT_MS = 24 * 60 * 60 * 1e3;
653
862
  function readPackageVersion(pkgJsonPath) {
654
863
  try {
655
- const raw = JSON.parse(fs5.readFileSync(pkgJsonPath, "utf-8"));
864
+ const raw = JSON.parse(fs6.readFileSync(pkgJsonPath, "utf-8"));
656
865
  if (typeof raw === "object" && raw !== null) {
657
866
  const version = raw["version"];
658
867
  if (typeof version === "string") return version;
@@ -662,9 +871,9 @@ var require_dist = __commonJS({
662
871
  return null;
663
872
  }
664
873
  __name(readPackageVersion, "readPackageVersion");
665
- var RootUpdateService = class _RootUpdateService {
874
+ var RootUpdateService = class {
666
875
  static {
667
- __name(this, "_RootUpdateService");
876
+ __name(this, "RootUpdateService");
668
877
  }
669
878
  spec;
670
879
  envNames;
@@ -683,7 +892,7 @@ var require_dist = __commonJS({
683
892
  this.envNames = options.envNames;
684
893
  this.logger = options.logger;
685
894
  this.restartServerFn = options.restartServer;
686
- this.dataDir = path5.resolve(options.dataDir);
895
+ this.dataDir = path6.resolve(options.dataDir);
687
896
  this.execNpm = options.execNpm ?? (async (args, opts) => {
688
897
  const { stdout } = await execFileAsync("npm", [
689
898
  ...args
@@ -719,63 +928,37 @@ var require_dist = __commonJS({
719
928
  seedVersion() {
720
929
  const seedDir = this.env[this.envNames.seedDir];
721
930
  if (seedDir === void 0 || seedDir.length === 0) return null;
722
- return readPackageVersion(path5.join(seedDir, "package.json"));
931
+ return readPackageVersion(path6.join(seedDir, "package.json"));
723
932
  }
724
933
  rootDir() {
725
934
  return serverRootDir(this.dataDir);
726
935
  }
727
- readState() {
728
- return readServerRootState(this.rootDir()) ?? emptyServerRootState();
729
- }
730
- /**
731
- * Like `readState`, but distinguishes "state.json missing" (normal on a
732
- * fresh node) from "state.json present but corrupt" — in the corrupt case
733
- * the starter booted the baked seed while installed data-dir versions are
734
- * silently ignored, which the status surface must make visible.
735
- */
736
- readStateInfo() {
737
- const rootDir = this.rootDir();
738
- const raw = readServerRootState(rootDir);
739
- if (raw !== null) return {
740
- state: raw,
741
- corrupt: false
742
- };
743
- return {
744
- state: emptyServerRootState(),
745
- corrupt: fs5.existsSync(stateFilePath(rootDir))
746
- };
747
- }
748
- updateState(state) {
936
+ updateState() {
749
937
  if (this.inFlight === "checking") return "checking";
750
938
  if (this.inFlight === "staging") return "staging";
751
- if (state.pendingBoot !== null) {
752
- const activeVersion = this.env[this.envNames.activeVersion] ?? null;
753
- if (activeVersion !== null && activeVersion === state.pendingBoot.version) {
754
- return "awaiting-confirmation";
755
- }
756
- return "pending-restart";
757
- }
939
+ if (readPendingRootSwap(this.rootDir()) !== null) return "pending-restart";
758
940
  return "idle";
759
941
  }
760
942
  async getServerPackageStatus() {
761
- const { state, corrupt } = this.readStateInfo();
762
943
  const runningVersion = this.runningVersion();
763
944
  const latestVersion = this.checkCache?.latestVersion ?? null;
945
+ const pending = readPendingRootSwap(this.rootDir());
764
946
  const updateAvailable = latestVersion !== null && runningVersion !== null && compareSemver(latestVersion, runningVersion) > 0;
765
947
  return {
766
948
  packageName: this.spec.packageName,
767
949
  runningVersion,
768
950
  nodeRuntimeVersion: process.versions.node,
769
- activeVersion: this.env[this.envNames.activeVersion] ?? state.currentVersion,
770
- previousVersion: state.previousVersion,
951
+ activeVersion: this.env[this.envNames.activeVersion] ?? runningVersion,
952
+ // Single-copy model: no N-1 retention, no rollback record.
953
+ previousVersion: null,
771
954
  seedVersion: this.seedVersion(),
772
955
  latestVersion,
773
956
  updateAvailable,
774
957
  bootMode: this.resolveBootMode(),
775
- updateState: this.updateState(state),
776
- pendingVersion: state.pendingBoot?.version ?? null,
777
- rolledBack: state.rolledBack,
778
- stateFileCorrupt: corrupt,
958
+ updateState: this.updateState(),
959
+ pendingVersion: pending?.version ?? null,
960
+ rolledBack: null,
961
+ stateFileCorrupt: false,
779
962
  lastCheckedAtMs: this.checkCache?.checkedAtMs ?? null
780
963
  };
781
964
  }
@@ -862,13 +1045,13 @@ var require_dist = __commonJS({
862
1045
  message: `Refused: another operation is in flight (${this.inFlight}).`
863
1046
  };
864
1047
  }
865
- const stateBefore = this.readState();
866
- if (stateBefore.pendingBoot !== null) {
1048
+ const pendingBefore = readPendingRootSwap(this.rootDir());
1049
+ if (pendingBefore !== null) {
867
1050
  return {
868
1051
  accepted: false,
869
1052
  targetVersion: input.version ?? null,
870
1053
  restarting: false,
871
- message: `Refused: version ${stateBefore.pendingBoot.version} is already staged and awaiting restart.`
1054
+ message: `Refused: version ${pendingBefore.version} is already staged and awaiting restart.`
872
1055
  };
873
1056
  }
874
1057
  let target = input.version ?? null;
@@ -895,7 +1078,7 @@ var require_dist = __commonJS({
895
1078
  }
896
1079
  if (isDevChannelVersion(target)) {
897
1080
  const uploadsDir = devUploadVersionDir(this.rootDir(), target);
898
- if (!fs5.existsSync(uploadsDir)) {
1081
+ if (!fs6.existsSync(uploadsDir)) {
899
1082
  return {
900
1083
  accepted: false,
901
1084
  targetVersion: target,
@@ -905,8 +1088,9 @@ var require_dist = __commonJS({
905
1088
  }
906
1089
  }
907
1090
  this.inFlight = "staging";
1091
+ let stagingPath;
908
1092
  try {
909
- await this.stageAndActivate(target);
1093
+ stagingPath = await this.stageClosure(target);
910
1094
  } catch (err) {
911
1095
  const message = err instanceof Error ? err.message : String(err);
912
1096
  this.logger.error("root package update staging failed", {
@@ -924,21 +1108,16 @@ var require_dist = __commonJS({
924
1108
  } finally {
925
1109
  this.inFlight = "idle";
926
1110
  }
927
- const state = this.readState();
928
- writeServerRootState(this.rootDir(), {
929
- ...state,
930
- pendingBoot: {
931
- version: target,
932
- fromVersion: state.currentVersion,
933
- requestedAtMs: this.now(),
934
- bootAttempts: 0
935
- },
936
- rolledBack: null
1111
+ writePendingRootSwap(this.rootDir(), {
1112
+ schemaVersion: 1,
1113
+ version: target,
1114
+ stagingPath,
1115
+ requestedAtMs: this.now()
937
1116
  });
938
1117
  this.logger.info("root package update staged \u2014 restarting to apply", {
939
1118
  meta: {
940
1119
  targetVersion: target,
941
- fromVersion: state.currentVersion
1120
+ fromVersion: runningVersion
942
1121
  }
943
1122
  });
944
1123
  this.restartServerFn(`${RESTART_REASON_PREFIX}: ${this.spec.packageName}@${target}`);
@@ -946,21 +1125,23 @@ var require_dist = __commonJS({
946
1125
  accepted: true,
947
1126
  targetVersion: target,
948
1127
  restarting: true,
949
- message: `Staged ${this.spec.packageName}@${target} \u2014 restarting to apply (probation boot with auto-rollback).`
1128
+ message: `Staged ${this.spec.packageName}@${target} \u2014 restarting to apply (single automatic reboot).`
950
1129
  };
951
1130
  }
952
1131
  /**
953
- * npm-install the target closure into a SAME-FS staging dir inside
954
- * `versions/`, validate it, then atomically rename it into place.
1132
+ * npm-install the target closure into a SAME-FS `.staging-*` dir, then
1133
+ * validate it (entry + version + native prebuilds). Returns the staging dir
1134
+ * path for the pending-root-swap marker; the STARTER swaps it into `current`
1135
+ * on the next boot. Throws (cleaning up the staging dir) on any failure so
1136
+ * `applyServerUpdate` reports a staging error and never arms a bad swap.
955
1137
  */
956
- async stageAndActivate(target) {
1138
+ async stageClosure(target) {
957
1139
  const rootDir = this.rootDir();
958
- const vDir = versionsDir(rootDir);
959
- fs5.mkdirSync(vDir, {
1140
+ fs6.mkdirSync(rootDir, {
960
1141
  recursive: true
961
1142
  });
962
- const stagingDir = path5.join(vDir, `.staging-${target}-${process.pid}-${this.now()}`);
963
- fs5.mkdirSync(stagingDir, {
1143
+ const stagingDir = stagingDirPath(rootDir, target, process.pid, this.now());
1144
+ fs6.mkdirSync(stagingDir, {
964
1145
  recursive: true
965
1146
  });
966
1147
  try {
@@ -973,8 +1154,8 @@ var require_dist = __commonJS({
973
1154
  "--no-fund",
974
1155
  "--loglevel=error"
975
1156
  ];
976
- if (fs5.existsSync(devDir)) {
977
- fs5.writeFileSync(path5.join(stagingDir, "package.json"), JSON.stringify(this.buildDevUploadsPackageJson(devDir, target), null, 2), "utf-8");
1157
+ if (fs6.existsSync(devDir)) {
1158
+ fs6.writeFileSync(path6.join(stagingDir, "package.json"), JSON.stringify(this.buildDevUploadsPackageJson(devDir, target), null, 2), "utf-8");
978
1159
  await this.execNpm([
979
1160
  ...installArgs,
980
1161
  ...buildNpmRegistryArgs(registry)
@@ -983,7 +1164,7 @@ var require_dist = __commonJS({
983
1164
  timeoutMs: NPM_INSTALL_TIMEOUT_MS
984
1165
  });
985
1166
  } else {
986
- fs5.writeFileSync(path5.join(stagingDir, "package.json"), JSON.stringify({
1167
+ fs6.writeFileSync(path6.join(stagingDir, "package.json"), JSON.stringify({
987
1168
  name: "camstack-node-root",
988
1169
  private: true
989
1170
  }, null, 2), "utf-8");
@@ -997,25 +1178,21 @@ var require_dist = __commonJS({
997
1178
  });
998
1179
  }
999
1180
  const entry = rootEntryPath2(stagingDir, this.spec);
1000
- if (!fs5.existsSync(entry)) {
1181
+ if (!fs6.existsSync(entry)) {
1001
1182
  throw new Error(`staged closure is missing the root entry (${entry})`);
1002
1183
  }
1003
- const stagedVersion = readPackageVersion(path5.join(rootPackageDir2(stagingDir, this.spec), "package.json"));
1004
- if (stagedVersion !== target) {
1005
- throw new Error(`staged closure version mismatch: expected ${target}, got ${stagedVersion ?? "unknown"}`);
1184
+ const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
1185
+ const invalid = validateClosureDir(stagingDir, nodeMajor, this.spec, target);
1186
+ if (invalid !== null) {
1187
+ throw new Error(`staged closure invalid: ${invalid}`);
1006
1188
  }
1007
- const dest = versionDir(rootDir, target);
1008
- if (fs5.existsSync(dest)) {
1009
- const aside = `${dest}.evicted-${this.now()}`;
1010
- await fs5.promises.rename(dest, aside);
1011
- await fs5.promises.rm(aside, {
1012
- recursive: true,
1013
- force: true
1014
- }).catch(() => void 0);
1189
+ const missingNatives = findMissingNativePrebuilds(stagingDir);
1190
+ if (missingNatives.length > 0) {
1191
+ throw new Error(`staged closure missing native prebuilds: ${missingNatives.join(", ")} \u2014 refusing to arm a swap that would break the forked runners`);
1015
1192
  }
1016
- await fs5.promises.rename(stagingDir, dest);
1193
+ return stagingDir;
1017
1194
  } catch (err) {
1018
- await fs5.promises.rm(stagingDir, {
1195
+ await fs6.promises.rm(stagingDir, {
1019
1196
  recursive: true,
1020
1197
  force: true
1021
1198
  }).catch(() => void 0);
@@ -1025,11 +1202,9 @@ var require_dist = __commonJS({
1025
1202
  /**
1026
1203
  * Synthetic staging package.json for the dev server-deploy channel:
1027
1204
  * `dependencies` carries the root package as a `file:` ref to its uploaded
1028
- * tgz; `overrides` maps EVERY OTHER uploaded `@camstack/<name>` to its tgz
1029
- * so any transitive occurrence resolves to the uploaded copy, never npm.
1205
+ * tgz; `overrides` maps EVERY OTHER uploaded `@camstack/<name>` to its tgz.
1030
1206
  * Fail-closed: a missing/invalid manifest, a manifest for a different
1031
- * version, a missing root package, or a listed-but-absent tgz all throw
1032
- * (surfaced by `applyServerUpdate` as a staging failure).
1207
+ * version, a missing root package, or a listed-but-absent tgz all throw.
1033
1208
  */
1034
1209
  buildDevUploadsPackageJson(devDir, target) {
1035
1210
  const manifest = readDevUploadManifest(devDir);
@@ -1044,8 +1219,8 @@ var require_dist = __commonJS({
1044
1219
  throw new Error(`dev-uploads for ${target} do not include the root package ${this.spec.packageName}`);
1045
1220
  }
1046
1221
  const fileRef = /* @__PURE__ */ __name((filename) => {
1047
- const abs = path5.join(devDir, filename);
1048
- if (!fs5.existsSync(abs)) {
1222
+ const abs = path6.join(devDir, filename);
1223
+ if (!fs6.existsSync(abs)) {
1049
1224
  throw new Error(`dev-uploads tarball listed in the manifest is missing: ${abs}`);
1050
1225
  }
1051
1226
  return `file:${abs}`;
@@ -1066,73 +1241,28 @@ var require_dist = __commonJS({
1066
1241
  } : {}
1067
1242
  };
1068
1243
  }
1069
- // ── Rollback ──────────────────────────────────────────────────────────
1244
+ // ── Rollback (removed — single-copy has no N-1) ────────────────────────
1245
+ /**
1246
+ * Rollback is NOT supported in the single-copy model — there is no retained
1247
+ * N-1 copy to revert to (operator's accepted tradeoff). Recovery is manual:
1248
+ * re-apply a known-good version via {@link applyServerUpdate}, or reinstall
1249
+ * the image seed. Kept on the surface so the cap contract is unchanged.
1250
+ */
1070
1251
  async rollbackServerUpdate() {
1071
- if (this.inFlight !== "idle") {
1072
- return {
1073
- accepted: false,
1074
- targetVersion: null,
1075
- restarting: false,
1076
- message: `Refused: another operation is in flight (${this.inFlight}).`
1077
- };
1078
- }
1079
- const state = this.readState();
1080
- if (state.pendingBoot !== null) {
1081
- return {
1082
- accepted: false,
1083
- targetVersion: null,
1084
- restarting: false,
1085
- message: `Refused: version ${state.pendingBoot.version} is already staged and awaiting restart.`
1086
- };
1087
- }
1088
- if (state.currentVersion === null || state.previousVersion === null) {
1089
- return {
1090
- accepted: false,
1091
- targetVersion: state.previousVersion,
1092
- restarting: false,
1093
- message: "Refused: no previous version available to roll back to."
1094
- };
1095
- }
1096
- const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
1097
- const invalidReason = validateVersionDir(this.rootDir(), state.previousVersion, nodeMajor, this.spec);
1098
- if (invalidReason !== null) {
1099
- return {
1100
- accepted: false,
1101
- targetVersion: state.previousVersion,
1102
- restarting: false,
1103
- message: `Refused: previous version ${state.previousVersion} is not loadable (${invalidReason}).`
1104
- };
1105
- }
1106
- writeServerRootState(this.rootDir(), {
1107
- ...state,
1108
- pendingBoot: {
1109
- version: state.previousVersion,
1110
- fromVersion: state.currentVersion,
1111
- requestedAtMs: this.now(),
1112
- bootAttempts: 0
1113
- }
1114
- });
1115
- this.logger.info("root package rollback requested \u2014 restarting", {
1116
- meta: {
1117
- fromVersion: state.currentVersion,
1118
- toVersion: state.previousVersion
1119
- }
1120
- });
1121
- this.restartServerFn(`${RESTART_REASON_PREFIX}: rollback to ${this.spec.packageName}@${state.previousVersion}`);
1122
1252
  return {
1123
- accepted: true,
1124
- targetVersion: state.previousVersion,
1125
- restarting: true,
1126
- message: `Rolling back to ${this.spec.packageName}@${state.previousVersion} \u2014 restarting.`
1253
+ accepted: false,
1254
+ targetVersion: null,
1255
+ restarting: false,
1256
+ 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."
1127
1257
  };
1128
1258
  }
1129
1259
  // ── Plain restart ─────────────────────────────────────────────────────
1130
1260
  /**
1131
- * Plain process restart — no version change. Backs the `restartServer` cap
1132
- * method. Guards mirror apply/rollback: refuse while a stage is in flight
1133
- * (a concurrent npm install must not be interrupted) or while a version is
1134
- * already staged awaiting restart (a naive bounce would boot the OLD version
1135
- * and orphan the pending one — the operator should apply/rollback instead).
1261
+ * Plain process restart — no version change. Refuse while a stage is in
1262
+ * flight (a concurrent npm install must not be interrupted) or while a
1263
+ * version is already staged awaiting restart (a naive bounce would boot the
1264
+ * OLD version and orphan the pending swap the operator should apply
1265
+ * instead).
1136
1266
  */
1137
1267
  restartNode() {
1138
1268
  const runningVersion = this.runningVersion();
@@ -1144,13 +1274,12 @@ var require_dist = __commonJS({
1144
1274
  message: `Refused: another operation is in flight (${this.inFlight}).`
1145
1275
  };
1146
1276
  }
1147
- const state = this.readState();
1148
- if (state.pendingBoot !== null) {
1277
+ if (readPendingRootSwap(this.rootDir()) !== null) {
1149
1278
  return {
1150
1279
  accepted: false,
1151
1280
  targetVersion: runningVersion,
1152
1281
  restarting: false,
1153
- message: `Refused: version ${state.pendingBoot.version} is staged and awaiting restart \u2014 apply or roll it back instead of a plain restart.`
1282
+ message: "Refused: a version is staged and awaiting restart \u2014 apply it instead of a plain restart."
1154
1283
  };
1155
1284
  }
1156
1285
  this.logger.info("plain node restart requested", {
@@ -1168,118 +1297,37 @@ var require_dist = __commonJS({
1168
1297
  }
1169
1298
  // ── Boot health confirmation ──────────────────────────────────────────
1170
1299
  /**
1171
- * Called by the post-boot path once the node is READY (hub: post-boot
1172
- * pipeline; agent: first acked `$hub.registerNode`). When THIS process is
1173
- * the probation boot of a pending version, promote it: `currentVersion`
1174
- * pending, `previousVersion` the version it replaced (N-1 retained for
1175
- * rollback), clear `pendingBoot` + `rolledBack`, and prune every version
1176
- * dir beyond {current, previous}. No-op otherwise.
1300
+ * Called by the post-boot path once the node is READY. In the single-copy
1301
+ * model there is no pending version to promote (the swap already happened in
1302
+ * the starter). This becomes a best-effort GC of orphaned transient dirs
1303
+ * (`.staging-*` / `.trash-*` left by a crash mid-swap) + the dev-uploads
1304
+ * sweep. Kept returning the legacy `{ promoted }` shape for the callers.
1177
1305
  */
1178
1306
  confirmBootHealthy() {
1179
- const state = readServerRootState(this.rootDir());
1180
- if (state === null || state.pendingBoot === null) return {
1181
- promoted: null
1182
- };
1183
- const activeVersion = this.env[this.envNames.activeVersion] ?? null;
1184
- if (activeVersion !== state.pendingBoot.version) {
1185
- return {
1186
- promoted: null
1187
- };
1188
- }
1189
- const promoted = {
1190
- ...state,
1191
- currentVersion: state.pendingBoot.version,
1192
- previousVersion: state.pendingBoot.fromVersion,
1193
- pendingBoot: null,
1194
- rolledBack: null
1195
- };
1196
- writeServerRootState(this.rootDir(), promoted);
1197
- this.pruneVersions([
1198
- promoted.currentVersion,
1199
- promoted.previousVersion
1200
- ].filter((v) => typeof v === "string"));
1307
+ sweepTransientRootDirs(this.rootDir(), this.now(), STALE_TRANSIENT_MS);
1201
1308
  this.sweepDevUploads(DEV_UPLOADS_KEEP_COUNT);
1202
- this.logger.info("root package update confirmed healthy", {
1203
- meta: {
1204
- version: promoted.currentVersion,
1205
- previousVersion: promoted.previousVersion
1206
- }
1207
- });
1208
1309
  return {
1209
- promoted: promoted.currentVersion
1310
+ promoted: null
1210
1311
  };
1211
1312
  }
1212
1313
  /**
1213
- * Remove version dirs under `versions/` not in `keep`.
1214
- *
1215
- * Transient dot-entries (`.staging-*` / `.evicted-*`) are NOT pruned unless
1216
- * older than {@link RootUpdateService.STALE_TRANSIENT_MS}: the API starts
1217
- * serving BEFORE the post-boot `confirmBootHealthy()` runs, so a concurrent
1218
- * `applyServerUpdate` can be mid-npm-install into a fresh `.staging-*` dir
1219
- * when this prune fires — deleting it would corrupt that install. Fresh
1220
- * transient dirs are owned by their in-flight operation (which cleans up on
1221
- * failure); the staleness sweep only collects dirs orphaned by a crash.
1222
- */
1223
- pruneVersions(keep) {
1224
- const vDir = versionsDir(this.rootDir());
1225
- let entries;
1226
- try {
1227
- entries = fs5.readdirSync(vDir);
1228
- } catch {
1229
- return;
1230
- }
1231
- for (const entry of entries) {
1232
- if (keep.includes(entry)) continue;
1233
- const full = path5.join(vDir, entry);
1234
- if (entry.startsWith(".")) {
1235
- try {
1236
- const ageMs = this.now() - fs5.statSync(full).mtimeMs;
1237
- if (ageMs < _RootUpdateService.STALE_TRANSIENT_MS) continue;
1238
- } catch {
1239
- continue;
1240
- }
1241
- }
1242
- try {
1243
- fs5.rmSync(full, {
1244
- recursive: true,
1245
- force: true
1246
- });
1247
- this.logger.debug("pruned server-root version dir", {
1248
- meta: {
1249
- entry
1250
- }
1251
- });
1252
- } catch (err) {
1253
- this.logger.warn("failed to prune server-root version dir", {
1254
- meta: {
1255
- entry,
1256
- error: err instanceof Error ? err.message : String(err)
1257
- }
1258
- });
1259
- }
1260
- }
1261
- }
1262
- /**
1263
1314
  * Promote-time sweep of `server-root/dev-uploads/`: keep only the
1264
1315
  * `keepCount` most recent entries (ordered by the self-describing
1265
1316
  * `-dev.<epochSeconds>` suffix; non-dev-shaped residue counts as oldest).
1266
- * Uses the same rename-aside graveyard pattern as the addon installer —
1267
- * a rename is a metadata op that succeeds even when a tarball is held
1268
- * open, and residue in the graveyard is re-swept on the next promote.
1269
1317
  */
1270
1318
  sweepDevUploads(keepCount) {
1271
1319
  const dir = devUploadsDir(this.rootDir());
1272
1320
  let entries;
1273
1321
  try {
1274
- entries = fs5.readdirSync(dir);
1322
+ entries = fs6.readdirSync(dir);
1275
1323
  } catch {
1276
1324
  return;
1277
1325
  }
1278
- const graveyard = path5.join(dir, ".sweeping");
1326
+ const graveyard = path6.join(dir, ".sweeping");
1279
1327
  try {
1280
- for (const residue of fs5.readdirSync(graveyard)) {
1328
+ for (const residue of fs6.readdirSync(graveyard)) {
1281
1329
  try {
1282
- fs5.rmSync(path5.join(graveyard, residue), {
1330
+ fs6.rmSync(path6.join(graveyard, residue), {
1283
1331
  recursive: true,
1284
1332
  force: true
1285
1333
  });
@@ -1294,13 +1342,13 @@ var require_dist = __commonJS({
1294
1342
  ].sort((a, b) => (devChannelEpoch(b) ?? -1) - (devChannelEpoch(a) ?? -1));
1295
1343
  const doomed = byNewestFirst.slice(keepCount);
1296
1344
  if (doomed.length === 0) return;
1297
- fs5.mkdirSync(graveyard, {
1345
+ fs6.mkdirSync(graveyard, {
1298
1346
  recursive: true
1299
1347
  });
1300
1348
  for (const entry of doomed) {
1301
- const aside = path5.join(graveyard, `${entry}-${this.now()}`);
1349
+ const aside = path6.join(graveyard, `${entry}-${this.now()}`);
1302
1350
  try {
1303
- fs5.renameSync(path5.join(dir, entry), aside);
1351
+ fs6.renameSync(path6.join(dir, entry), aside);
1304
1352
  } catch (err) {
1305
1353
  this.logger.warn("failed to move dev-uploads entry aside for sweep", {
1306
1354
  meta: {
@@ -1311,7 +1359,7 @@ var require_dist = __commonJS({
1311
1359
  continue;
1312
1360
  }
1313
1361
  try {
1314
- fs5.rmSync(aside, {
1362
+ fs6.rmSync(aside, {
1315
1363
  recursive: true,
1316
1364
  force: true
1317
1365
  });
@@ -1324,8 +1372,6 @@ var require_dist = __commonJS({
1324
1372
  }
1325
1373
  }
1326
1374
  }
1327
- /** Transient (.staging- / .evicted-) dirs younger than this are never pruned. */
1328
- static STALE_TRANSIENT_MS = 24 * 60 * 60 * 1e3;
1329
1375
  };
1330
1376
  }
1331
1377
  });