@camstack/server 1.1.70 → 1.1.71

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,304 @@ 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"));
374
+ var CURRENT_DIRNAME = "current";
375
+ var PENDING_ROOT_SWAP_FILE = ".pending-root-swap.json";
376
+ var LEGACY_FRAMEWORK_MARKERS = [
377
+ ".pending-framework-swap.json",
378
+ ".framework-swap-confirm.json"
379
+ ];
380
+ function currentDir(rootDir) {
381
+ return path3.join(rootDir, CURRENT_DIRNAME);
382
+ }
383
+ __name(currentDir, "currentDir");
384
+ function currentEntryPath(rootDir, spec) {
385
+ return rootEntryPath2(currentDir(rootDir), spec);
386
+ }
387
+ __name(currentEntryPath, "currentEntryPath");
388
+ function stagingDirPath(rootDir, target, pid, now) {
389
+ return path3.join(rootDir, `.staging-${target}-${pid}-${now}`);
390
+ }
391
+ __name(stagingDirPath, "stagingDirPath");
392
+ function pendingRootSwapPath(rootDir) {
393
+ return path3.join(rootDir, PENDING_ROOT_SWAP_FILE);
394
+ }
395
+ __name(pendingRootSwapPath, "pendingRootSwapPath");
396
+ function isPendingRootSwap(v) {
397
+ if (typeof v !== "object" || v === null) return false;
398
+ const m = v;
399
+ return m["schemaVersion"] === 1 && typeof m["version"] === "string" && typeof m["stagingPath"] === "string" && typeof m["requestedAtMs"] === "number";
400
+ }
401
+ __name(isPendingRootSwap, "isPendingRootSwap");
402
+ function readPendingRootSwap(rootDir) {
403
+ try {
404
+ const raw = JSON.parse(fs3.readFileSync(pendingRootSwapPath(rootDir), "utf-8"));
405
+ return isPendingRootSwap(raw) ? raw : null;
406
+ } catch {
407
+ return null;
408
+ }
409
+ }
410
+ __name(readPendingRootSwap, "readPendingRootSwap");
411
+ function writePendingRootSwap(rootDir, marker) {
412
+ fs3.mkdirSync(rootDir, {
413
+ recursive: true
414
+ });
415
+ const target = pendingRootSwapPath(rootDir);
416
+ const tmp = `${target}.tmp`;
417
+ fs3.writeFileSync(tmp, JSON.stringify(marker, null, 2), "utf-8");
418
+ fs3.renameSync(tmp, target);
419
+ }
420
+ __name(writePendingRootSwap, "writePendingRootSwap");
421
+ function clearPendingRootSwap(rootDir) {
422
+ try {
423
+ fs3.rmSync(pendingRootSwapPath(rootDir), {
424
+ force: true
425
+ });
426
+ } catch {
427
+ }
428
+ }
429
+ __name(clearPendingRootSwap, "clearPendingRootSwap");
430
+ var REQUIRED_NATIVE_PACKAGES = [
431
+ "better-sqlite3",
432
+ "sharp",
433
+ "node-av"
434
+ ];
435
+ var NATIVE_SEARCH_DIRS = {
436
+ "better-sqlite3": [
437
+ "better-sqlite3"
438
+ ],
439
+ sharp: [
440
+ "sharp",
441
+ "@img"
442
+ ],
443
+ "node-av": [
444
+ "node-av",
445
+ "@seydx"
446
+ ]
447
+ };
448
+ function hasDotNode(dir, maxDepth) {
449
+ let entries;
450
+ try {
451
+ entries = fs3.readdirSync(dir, {
452
+ withFileTypes: true
453
+ });
454
+ } catch {
455
+ return false;
456
+ }
457
+ for (const entry of entries) {
458
+ if (entry.isFile() && entry.name.endsWith(".node")) return true;
459
+ }
460
+ if (maxDepth <= 0) return false;
461
+ for (const entry of entries) {
462
+ if (entry.isDirectory() && hasDotNode(path3.join(dir, entry.name), maxDepth - 1)) return true;
463
+ }
464
+ return false;
465
+ }
466
+ __name(hasDotNode, "hasDotNode");
467
+ function findMissingNativePrebuilds(closureDir) {
468
+ const nm = path3.join(closureDir, "node_modules");
469
+ const missing = [];
470
+ for (const pkg of REQUIRED_NATIVE_PACKAGES) {
471
+ const searchDirs = NATIVE_SEARCH_DIRS[pkg] ?? [
472
+ pkg
473
+ ];
474
+ const found = searchDirs.some((rel) => hasDotNode(path3.join(nm, ...rel.split("/")), 4));
475
+ if (!found) missing.push(pkg);
476
+ }
477
+ return missing;
478
+ }
479
+ __name(findMissingNativePrebuilds, "findMissingNativePrebuilds");
480
+ function rmrf(target) {
481
+ try {
482
+ fs3.rmSync(target, {
483
+ recursive: true,
484
+ force: true
485
+ });
486
+ } catch {
487
+ }
488
+ }
489
+ __name(rmrf, "rmrf");
490
+ function errMsg(err) {
491
+ return err instanceof Error ? err.message : String(err);
492
+ }
493
+ __name(errMsg, "errMsg");
494
+ function applyPendingRootSwap(rootDir, spec, nodeMajor, now, log) {
495
+ const marker = readPendingRootSwap(rootDir);
496
+ if (marker === null) return {
497
+ applied: false,
498
+ version: null,
499
+ reason: "no pending swap"
500
+ };
501
+ const staging = marker.stagingPath;
502
+ const dest = currentDir(rootDir);
503
+ const invalid = validateClosureDir(staging, nodeMajor, spec, marker.version);
504
+ if (invalid !== null) {
505
+ if (!fs3.existsSync(staging) && validateClosureDir(dest, nodeMajor, spec, marker.version) === null) {
506
+ clearPendingRootSwap(rootDir);
507
+ log(`[single-copy] pending swap ${marker.version} already applied \u2014 cleared marker`);
508
+ return {
509
+ applied: true,
510
+ version: marker.version,
511
+ reason: "already applied"
512
+ };
513
+ }
514
+ log(`[single-copy] staged swap ${marker.version} invalid \u2014 discarding (${invalid})`);
515
+ rmrf(staging);
516
+ clearPendingRootSwap(rootDir);
517
+ return {
518
+ applied: false,
519
+ version: null,
520
+ reason: invalid
521
+ };
522
+ }
523
+ const missing = findMissingNativePrebuilds(staging);
524
+ if (missing.length > 0) {
525
+ log(`[single-copy] staged swap ${marker.version} missing native prebuilds: ${missing.join(", ")} \u2014 discarding`);
526
+ rmrf(staging);
527
+ clearPendingRootSwap(rootDir);
528
+ return {
529
+ applied: false,
530
+ version: null,
531
+ reason: `missing native prebuilds: ${missing.join(", ")}`
532
+ };
533
+ }
534
+ let trash = null;
535
+ try {
536
+ if (fs3.existsSync(dest)) {
537
+ trash = path3.join(rootDir, `.trash-${now()}-${process.pid}`);
538
+ fs3.renameSync(dest, trash);
539
+ }
540
+ try {
541
+ fs3.renameSync(staging, dest);
542
+ } catch (err) {
543
+ if (trash !== null && !fs3.existsSync(dest)) {
544
+ try {
545
+ fs3.renameSync(trash, dest);
546
+ } catch {
547
+ }
548
+ }
549
+ throw err;
550
+ }
551
+ } catch (err) {
552
+ log(`[single-copy] swap of ${marker.version} FAILED \u2014 current preserved (${errMsg(err)})`);
553
+ return {
554
+ applied: false,
555
+ version: null,
556
+ reason: errMsg(err)
557
+ };
558
+ }
559
+ if (trash !== null) rmrf(trash);
560
+ clearPendingRootSwap(rootDir);
561
+ log(`[single-copy] applied staged root swap \u2192 ${marker.version}`);
562
+ return {
563
+ applied: true,
564
+ version: marker.version,
565
+ reason: "applied"
566
+ };
567
+ }
568
+ __name(applyPendingRootSwap, "applyPendingRootSwap");
569
+ function migrateToSingleCopy(rootDir, spec, nodeMajor, now, log) {
570
+ const dest = currentDir(rootDir);
571
+ if (fs3.existsSync(dest) && validateClosureDir(dest, nodeMajor, spec) === null) {
572
+ return {
573
+ migrated: false,
574
+ version: null,
575
+ reason: "current already present"
576
+ };
577
+ }
578
+ const candidates = [];
579
+ const legacyState = readServerRootState(rootDir);
580
+ if (legacyState !== null) {
581
+ for (const v of [
582
+ legacyState.currentVersion,
583
+ legacyState.pendingBoot?.version ?? null,
584
+ legacyState.previousVersion
585
+ ]) {
586
+ if (typeof v === "string" && !candidates.includes(v)) candidates.push(v);
587
+ }
588
+ }
589
+ try {
590
+ for (const entry of fs3.readdirSync(versionsDir(rootDir))) {
591
+ if (!entry.startsWith(".") && !candidates.includes(entry)) candidates.push(entry);
592
+ }
593
+ } catch {
594
+ }
595
+ const source = candidates.find((v) => validateClosureDir(versionDir(rootDir, v), nodeMajor, spec, v) === null);
596
+ if (source === void 0) {
597
+ return {
598
+ migrated: false,
599
+ version: null,
600
+ reason: "no valid legacy versions/<X> to adopt"
601
+ };
602
+ }
603
+ if (fs3.existsSync(dest)) {
604
+ const aside = path3.join(rootDir, `.trash-${now()}-${process.pid}-stale-current`);
605
+ try {
606
+ fs3.renameSync(dest, aside);
607
+ } catch {
608
+ rmrf(dest);
609
+ }
610
+ }
611
+ try {
612
+ fs3.renameSync(versionDir(rootDir, source), dest);
613
+ } catch (err) {
614
+ log(`[single-copy] migration rename of versions/${source} failed (${errMsg(err)})`);
615
+ return {
616
+ migrated: false,
617
+ version: null,
618
+ reason: errMsg(err)
619
+ };
620
+ }
621
+ rmrf(versionsDir(rootDir));
622
+ rmrf(stateFilePath(rootDir));
623
+ const dataDir = path3.dirname(rootDir);
624
+ for (const marker of LEGACY_FRAMEWORK_MARKERS) rmrf(path3.join(dataDir, marker));
625
+ log(`[single-copy] migrated legacy versions/${source} \u2192 current`);
626
+ return {
627
+ migrated: true,
628
+ version: source,
629
+ reason: "adopted legacy version"
630
+ };
631
+ }
632
+ __name(migrateToSingleCopy, "migrateToSingleCopy");
633
+ function sweepTransientRootDirs(rootDir, now, staleMs) {
634
+ let entries;
635
+ try {
636
+ entries = fs3.readdirSync(rootDir);
637
+ } catch {
638
+ return;
639
+ }
640
+ for (const entry of entries) {
641
+ if (!entry.startsWith(".staging-") && !entry.startsWith(".trash-")) continue;
642
+ const full = path3.join(rootDir, entry);
643
+ try {
644
+ if (now - fs3.statSync(full).mtimeMs < staleMs) continue;
645
+ } catch {
646
+ continue;
647
+ }
648
+ rmrf(full);
649
+ }
650
+ }
651
+ __name(sweepTransientRootDirs, "sweepTransientRootDirs");
652
+ function planBoot(currentValid) {
653
+ if (currentValid) {
654
+ return {
655
+ kind: "current",
656
+ reason: "single-copy closure present and valid"
657
+ };
658
+ }
659
+ return {
660
+ kind: "baked",
661
+ reason: "no valid single-copy closure \u2014 booting the baked seed"
662
+ };
663
+ }
664
+ __name(planBoot, "planBoot");
355
665
  function parse(version) {
356
666
  const dashIdx = version.indexOf("-");
357
667
  const base = dashIdx === -1 ? version : version.slice(0, dashIdx);
@@ -384,142 +694,27 @@ var require_dist = __commonJS({
384
694
  return 0;
385
695
  }
386
696
  __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
- };
394
- }
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 {
424
- 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
- }
435
- };
436
- }
437
- }
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
- };
452
- }
453
- return {
454
- kind: "data-root",
455
- version: next.currentVersion,
456
- probation: false,
457
- stateToWrite: changed ? next : null
458
- };
459
- }
460
- const broken = next.currentVersion;
461
- if (next.previousVersion !== null && isValidVersion(next.previousVersion)) {
462
- const fallback = next.previousVersion;
463
- 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
- }
478
- };
479
- }
480
- 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
- }
493
- };
494
- }
495
- return {
496
- kind: "baked",
497
- reason: "no active data-root version",
498
- stateToWrite: changed ? next : null
499
- };
500
- }
501
- __name(planBoot, "planBoot");
502
- var fs3 = __toESM2(require("fs"));
503
- var path3 = __toESM2(require("path"));
697
+ var fs4 = __toESM2(require("fs"));
698
+ var path4 = __toESM2(require("path"));
504
699
  function detectWorkspaceRoot(fromDir) {
505
- let dir = path3.resolve(fromDir);
700
+ let dir = path4.resolve(fromDir);
506
701
  for (; ; ) {
507
- const pkgPath = path3.join(dir, "package.json");
702
+ const pkgPath = path4.join(dir, "package.json");
508
703
  try {
509
- const raw = JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
704
+ const raw = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
510
705
  if (typeof raw === "object" && raw !== null && raw["workspaces"] !== void 0) {
511
706
  return dir;
512
707
  }
513
708
  } catch {
514
709
  }
515
- const parent = path3.dirname(dir);
710
+ const parent = path4.dirname(dir);
516
711
  if (parent === dir) return null;
517
712
  dir = parent;
518
713
  }
519
714
  }
520
715
  __name(detectWorkspaceRoot, "detectWorkspaceRoot");
521
- var fs4 = __toESM2(require("fs"));
522
- var path4 = __toESM2(require("path"));
716
+ var fs5 = __toESM2(require("fs"));
717
+ var path5 = __toESM2(require("path"));
523
718
  var import_node_url = require("url");
524
719
  var HOST_EXTERNAL_SPECIFIERS = [
525
720
  "@camstack/system",
@@ -543,7 +738,7 @@ var require_dist = __commonJS({
543
738
  console.warn("[starter] module.registerHooks unavailable (Node < 22.15) \u2014 host-external redirect skipped");
544
739
  return;
545
740
  }
546
- const anchorURL = (0, import_node_url.pathToFileURL)(path4.join(activeRootDir, "node_modules", "__camstack_starter_anchor__.js")).href;
741
+ const anchorURL = (0, import_node_url.pathToFileURL)(path5.join(activeRootDir, "node_modules", "__camstack_starter_anchor__.js")).href;
547
742
  const hooks = {
548
743
  resolve: /* @__PURE__ */ __name((specifier, context, nextResolve) => {
549
744
  if (isHostExternal(specifier)) {
@@ -561,24 +756,23 @@ var require_dist = __commonJS({
561
756
  registerHooks(hooks);
562
757
  }
563
758
  __name(registerActiveRootResolver, "registerActiveRootResolver");
564
- function readSeedVersion(seedEntry) {
759
+ function readClosureVersion(closureDir, spec) {
565
760
  try {
566
- const pkgJsonPath = path4.join(path4.dirname(seedEntry), "..", "package.json");
567
- const raw = JSON.parse(fs4.readFileSync(pkgJsonPath, "utf-8"));
761
+ const raw = JSON.parse(fs5.readFileSync(path5.join(rootPackageDir2(closureDir, spec), "package.json"), "utf-8"));
568
762
  if (typeof raw === "object" && raw !== null) {
569
763
  const version = raw["version"];
570
764
  if (typeof version === "string") return version;
571
765
  }
572
- return null;
573
766
  } catch {
574
- return null;
575
767
  }
768
+ return null;
576
769
  }
577
- __name(readSeedVersion, "readSeedVersion");
770
+ __name(readClosureVersion, "readClosureVersion");
578
771
  function runNodeStarter(options) {
579
772
  const env = options.env ?? process.env;
580
773
  const now = options.now ?? Date.now;
581
774
  const registerResolver = options.registerResolver ?? registerActiveRootResolver;
775
+ const nodeMajor = options.nodeMajor ?? Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
582
776
  const disabled = env[options.envNames.killSwitch] === "off";
583
777
  const workspaceRoot = detectWorkspaceRoot(options.starterDir);
584
778
  if (workspaceRoot !== null) {
@@ -594,36 +788,24 @@ var require_dist = __commonJS({
594
788
  return;
595
789
  }
596
790
  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
- }
791
+ const swap = applyPendingRootSwap(rootDir, options.spec, nodeMajor, now, (m) => console.log(m));
792
+ if (swap.applied) console.log(`[starter] applied staged update \u2192 ${swap.version ?? "?"}`);
793
+ else if (swap.reason !== "no pending swap") console.warn(`[starter] staged update NOT applied: ${swap.reason}`);
794
+ const migration = migrateToSingleCopy(rootDir, options.spec, nodeMajor, now, (m) => console.log(m));
795
+ if (migration.migrated) console.log(`[starter] migrated legacy layout \u2192 single copy (${migration.version ?? "?"})`);
796
+ const activeRootDir = currentDir(rootDir);
797
+ const invalid = validateClosureDir(activeRootDir, nodeMajor, options.spec);
798
+ if (invalid !== null && fs5.existsSync(activeRootDir)) {
799
+ console.warn(`[starter] single copy invalid: ${invalid}`);
619
800
  }
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)" : ""));
801
+ const plan = planBoot(invalid === null);
802
+ if (plan.kind === "current") {
803
+ const version = readClosureVersion(activeRootDir, options.spec);
804
+ const entry = currentEntryPath(rootDir, options.spec);
805
+ console.log(`[starter] booting ${options.spec.packageName}@${version ?? "?"} from ${activeRootDir}`);
624
806
  env[options.envNames.bootMode] = "data-root";
625
807
  env[options.envNames.activeRoot] = activeRootDir;
626
- env[options.envNames.activeVersion] = plan.version;
808
+ if (version !== null) env[options.envNames.activeVersion] = version;
627
809
  registerResolver(activeRootDir);
628
810
  options.loadEntry(entry);
629
811
  return;
@@ -634,8 +816,8 @@ var require_dist = __commonJS({
634
816
  }
635
817
  __name(runNodeStarter, "runNodeStarter");
636
818
  var import_node_child_process = require("child_process");
637
- var fs5 = __toESM2(require("fs"));
638
- var path5 = __toESM2(require("path"));
819
+ var fs6 = __toESM2(require("fs"));
820
+ var path6 = __toESM2(require("path"));
639
821
  var import_node_util = require("util");
640
822
  var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
641
823
  function buildNpmRegistryArgs(registry) {
@@ -650,9 +832,10 @@ var require_dist = __commonJS({
650
832
  var NPM_VIEW_TIMEOUT_MS = 2e4;
651
833
  var NPM_INSTALL_TIMEOUT_MS = 15 * 6e4;
652
834
  var RESTART_REASON_PREFIX = "server-update";
835
+ var STALE_TRANSIENT_MS = 24 * 60 * 60 * 1e3;
653
836
  function readPackageVersion(pkgJsonPath) {
654
837
  try {
655
- const raw = JSON.parse(fs5.readFileSync(pkgJsonPath, "utf-8"));
838
+ const raw = JSON.parse(fs6.readFileSync(pkgJsonPath, "utf-8"));
656
839
  if (typeof raw === "object" && raw !== null) {
657
840
  const version = raw["version"];
658
841
  if (typeof version === "string") return version;
@@ -662,9 +845,9 @@ var require_dist = __commonJS({
662
845
  return null;
663
846
  }
664
847
  __name(readPackageVersion, "readPackageVersion");
665
- var RootUpdateService = class _RootUpdateService {
848
+ var RootUpdateService = class {
666
849
  static {
667
- __name(this, "_RootUpdateService");
850
+ __name(this, "RootUpdateService");
668
851
  }
669
852
  spec;
670
853
  envNames;
@@ -683,7 +866,7 @@ var require_dist = __commonJS({
683
866
  this.envNames = options.envNames;
684
867
  this.logger = options.logger;
685
868
  this.restartServerFn = options.restartServer;
686
- this.dataDir = path5.resolve(options.dataDir);
869
+ this.dataDir = path6.resolve(options.dataDir);
687
870
  this.execNpm = options.execNpm ?? (async (args, opts) => {
688
871
  const { stdout } = await execFileAsync("npm", [
689
872
  ...args
@@ -719,63 +902,37 @@ var require_dist = __commonJS({
719
902
  seedVersion() {
720
903
  const seedDir = this.env[this.envNames.seedDir];
721
904
  if (seedDir === void 0 || seedDir.length === 0) return null;
722
- return readPackageVersion(path5.join(seedDir, "package.json"));
905
+ return readPackageVersion(path6.join(seedDir, "package.json"));
723
906
  }
724
907
  rootDir() {
725
908
  return serverRootDir(this.dataDir);
726
909
  }
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) {
910
+ updateState() {
749
911
  if (this.inFlight === "checking") return "checking";
750
912
  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
- }
913
+ if (readPendingRootSwap(this.rootDir()) !== null) return "pending-restart";
758
914
  return "idle";
759
915
  }
760
916
  async getServerPackageStatus() {
761
- const { state, corrupt } = this.readStateInfo();
762
917
  const runningVersion = this.runningVersion();
763
918
  const latestVersion = this.checkCache?.latestVersion ?? null;
919
+ const pending = readPendingRootSwap(this.rootDir());
764
920
  const updateAvailable = latestVersion !== null && runningVersion !== null && compareSemver(latestVersion, runningVersion) > 0;
765
921
  return {
766
922
  packageName: this.spec.packageName,
767
923
  runningVersion,
768
924
  nodeRuntimeVersion: process.versions.node,
769
- activeVersion: this.env[this.envNames.activeVersion] ?? state.currentVersion,
770
- previousVersion: state.previousVersion,
925
+ activeVersion: this.env[this.envNames.activeVersion] ?? runningVersion,
926
+ // Single-copy model: no N-1 retention, no rollback record.
927
+ previousVersion: null,
771
928
  seedVersion: this.seedVersion(),
772
929
  latestVersion,
773
930
  updateAvailable,
774
931
  bootMode: this.resolveBootMode(),
775
- updateState: this.updateState(state),
776
- pendingVersion: state.pendingBoot?.version ?? null,
777
- rolledBack: state.rolledBack,
778
- stateFileCorrupt: corrupt,
932
+ updateState: this.updateState(),
933
+ pendingVersion: pending?.version ?? null,
934
+ rolledBack: null,
935
+ stateFileCorrupt: false,
779
936
  lastCheckedAtMs: this.checkCache?.checkedAtMs ?? null
780
937
  };
781
938
  }
@@ -862,13 +1019,13 @@ var require_dist = __commonJS({
862
1019
  message: `Refused: another operation is in flight (${this.inFlight}).`
863
1020
  };
864
1021
  }
865
- const stateBefore = this.readState();
866
- if (stateBefore.pendingBoot !== null) {
1022
+ const pendingBefore = readPendingRootSwap(this.rootDir());
1023
+ if (pendingBefore !== null) {
867
1024
  return {
868
1025
  accepted: false,
869
1026
  targetVersion: input.version ?? null,
870
1027
  restarting: false,
871
- message: `Refused: version ${stateBefore.pendingBoot.version} is already staged and awaiting restart.`
1028
+ message: `Refused: version ${pendingBefore.version} is already staged and awaiting restart.`
872
1029
  };
873
1030
  }
874
1031
  let target = input.version ?? null;
@@ -895,7 +1052,7 @@ var require_dist = __commonJS({
895
1052
  }
896
1053
  if (isDevChannelVersion(target)) {
897
1054
  const uploadsDir = devUploadVersionDir(this.rootDir(), target);
898
- if (!fs5.existsSync(uploadsDir)) {
1055
+ if (!fs6.existsSync(uploadsDir)) {
899
1056
  return {
900
1057
  accepted: false,
901
1058
  targetVersion: target,
@@ -905,8 +1062,9 @@ var require_dist = __commonJS({
905
1062
  }
906
1063
  }
907
1064
  this.inFlight = "staging";
1065
+ let stagingPath;
908
1066
  try {
909
- await this.stageAndActivate(target);
1067
+ stagingPath = await this.stageClosure(target);
910
1068
  } catch (err) {
911
1069
  const message = err instanceof Error ? err.message : String(err);
912
1070
  this.logger.error("root package update staging failed", {
@@ -924,21 +1082,16 @@ var require_dist = __commonJS({
924
1082
  } finally {
925
1083
  this.inFlight = "idle";
926
1084
  }
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
1085
+ writePendingRootSwap(this.rootDir(), {
1086
+ schemaVersion: 1,
1087
+ version: target,
1088
+ stagingPath,
1089
+ requestedAtMs: this.now()
937
1090
  });
938
1091
  this.logger.info("root package update staged \u2014 restarting to apply", {
939
1092
  meta: {
940
1093
  targetVersion: target,
941
- fromVersion: state.currentVersion
1094
+ fromVersion: runningVersion
942
1095
  }
943
1096
  });
944
1097
  this.restartServerFn(`${RESTART_REASON_PREFIX}: ${this.spec.packageName}@${target}`);
@@ -946,21 +1099,23 @@ var require_dist = __commonJS({
946
1099
  accepted: true,
947
1100
  targetVersion: target,
948
1101
  restarting: true,
949
- message: `Staged ${this.spec.packageName}@${target} \u2014 restarting to apply (probation boot with auto-rollback).`
1102
+ message: `Staged ${this.spec.packageName}@${target} \u2014 restarting to apply (single automatic reboot).`
950
1103
  };
951
1104
  }
952
1105
  /**
953
- * npm-install the target closure into a SAME-FS staging dir inside
954
- * `versions/`, validate it, then atomically rename it into place.
1106
+ * npm-install the target closure into a SAME-FS `.staging-*` dir, then
1107
+ * validate it (entry + version + native prebuilds). Returns the staging dir
1108
+ * path for the pending-root-swap marker; the STARTER swaps it into `current`
1109
+ * on the next boot. Throws (cleaning up the staging dir) on any failure so
1110
+ * `applyServerUpdate` reports a staging error and never arms a bad swap.
955
1111
  */
956
- async stageAndActivate(target) {
1112
+ async stageClosure(target) {
957
1113
  const rootDir = this.rootDir();
958
- const vDir = versionsDir(rootDir);
959
- fs5.mkdirSync(vDir, {
1114
+ fs6.mkdirSync(rootDir, {
960
1115
  recursive: true
961
1116
  });
962
- const stagingDir = path5.join(vDir, `.staging-${target}-${process.pid}-${this.now()}`);
963
- fs5.mkdirSync(stagingDir, {
1117
+ const stagingDir = stagingDirPath(rootDir, target, process.pid, this.now());
1118
+ fs6.mkdirSync(stagingDir, {
964
1119
  recursive: true
965
1120
  });
966
1121
  try {
@@ -973,8 +1128,8 @@ var require_dist = __commonJS({
973
1128
  "--no-fund",
974
1129
  "--loglevel=error"
975
1130
  ];
976
- if (fs5.existsSync(devDir)) {
977
- fs5.writeFileSync(path5.join(stagingDir, "package.json"), JSON.stringify(this.buildDevUploadsPackageJson(devDir, target), null, 2), "utf-8");
1131
+ if (fs6.existsSync(devDir)) {
1132
+ fs6.writeFileSync(path6.join(stagingDir, "package.json"), JSON.stringify(this.buildDevUploadsPackageJson(devDir, target), null, 2), "utf-8");
978
1133
  await this.execNpm([
979
1134
  ...installArgs,
980
1135
  ...buildNpmRegistryArgs(registry)
@@ -983,7 +1138,7 @@ var require_dist = __commonJS({
983
1138
  timeoutMs: NPM_INSTALL_TIMEOUT_MS
984
1139
  });
985
1140
  } else {
986
- fs5.writeFileSync(path5.join(stagingDir, "package.json"), JSON.stringify({
1141
+ fs6.writeFileSync(path6.join(stagingDir, "package.json"), JSON.stringify({
987
1142
  name: "camstack-node-root",
988
1143
  private: true
989
1144
  }, null, 2), "utf-8");
@@ -997,25 +1152,21 @@ var require_dist = __commonJS({
997
1152
  });
998
1153
  }
999
1154
  const entry = rootEntryPath2(stagingDir, this.spec);
1000
- if (!fs5.existsSync(entry)) {
1155
+ if (!fs6.existsSync(entry)) {
1001
1156
  throw new Error(`staged closure is missing the root entry (${entry})`);
1002
1157
  }
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"}`);
1158
+ const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
1159
+ const invalid = validateClosureDir(stagingDir, nodeMajor, this.spec, target);
1160
+ if (invalid !== null) {
1161
+ throw new Error(`staged closure invalid: ${invalid}`);
1006
1162
  }
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);
1163
+ const missingNatives = findMissingNativePrebuilds(stagingDir);
1164
+ if (missingNatives.length > 0) {
1165
+ throw new Error(`staged closure missing native prebuilds: ${missingNatives.join(", ")} \u2014 refusing to arm a swap that would break the forked runners`);
1015
1166
  }
1016
- await fs5.promises.rename(stagingDir, dest);
1167
+ return stagingDir;
1017
1168
  } catch (err) {
1018
- await fs5.promises.rm(stagingDir, {
1169
+ await fs6.promises.rm(stagingDir, {
1019
1170
  recursive: true,
1020
1171
  force: true
1021
1172
  }).catch(() => void 0);
@@ -1025,11 +1176,9 @@ var require_dist = __commonJS({
1025
1176
  /**
1026
1177
  * Synthetic staging package.json for the dev server-deploy channel:
1027
1178
  * `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.
1179
+ * tgz; `overrides` maps EVERY OTHER uploaded `@camstack/<name>` to its tgz.
1030
1180
  * 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).
1181
+ * version, a missing root package, or a listed-but-absent tgz all throw.
1033
1182
  */
1034
1183
  buildDevUploadsPackageJson(devDir, target) {
1035
1184
  const manifest = readDevUploadManifest(devDir);
@@ -1044,8 +1193,8 @@ var require_dist = __commonJS({
1044
1193
  throw new Error(`dev-uploads for ${target} do not include the root package ${this.spec.packageName}`);
1045
1194
  }
1046
1195
  const fileRef = /* @__PURE__ */ __name((filename) => {
1047
- const abs = path5.join(devDir, filename);
1048
- if (!fs5.existsSync(abs)) {
1196
+ const abs = path6.join(devDir, filename);
1197
+ if (!fs6.existsSync(abs)) {
1049
1198
  throw new Error(`dev-uploads tarball listed in the manifest is missing: ${abs}`);
1050
1199
  }
1051
1200
  return `file:${abs}`;
@@ -1066,73 +1215,28 @@ var require_dist = __commonJS({
1066
1215
  } : {}
1067
1216
  };
1068
1217
  }
1069
- // ── Rollback ──────────────────────────────────────────────────────────
1218
+ // ── Rollback (removed — single-copy has no N-1) ────────────────────────
1219
+ /**
1220
+ * Rollback is NOT supported in the single-copy model — there is no retained
1221
+ * N-1 copy to revert to (operator's accepted tradeoff). Recovery is manual:
1222
+ * re-apply a known-good version via {@link applyServerUpdate}, or reinstall
1223
+ * the image seed. Kept on the surface so the cap contract is unchanged.
1224
+ */
1070
1225
  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
1226
  return {
1123
- accepted: true,
1124
- targetVersion: state.previousVersion,
1125
- restarting: true,
1126
- message: `Rolling back to ${this.spec.packageName}@${state.previousVersion} \u2014 restarting.`
1227
+ accepted: false,
1228
+ targetVersion: null,
1229
+ restarting: false,
1230
+ 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
1231
  };
1128
1232
  }
1129
1233
  // ── Plain restart ─────────────────────────────────────────────────────
1130
1234
  /**
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).
1235
+ * Plain process restart — no version change. Refuse while a stage is in
1236
+ * flight (a concurrent npm install must not be interrupted) or while a
1237
+ * version is already staged awaiting restart (a naive bounce would boot the
1238
+ * OLD version and orphan the pending swap the operator should apply
1239
+ * instead).
1136
1240
  */
1137
1241
  restartNode() {
1138
1242
  const runningVersion = this.runningVersion();
@@ -1144,13 +1248,12 @@ var require_dist = __commonJS({
1144
1248
  message: `Refused: another operation is in flight (${this.inFlight}).`
1145
1249
  };
1146
1250
  }
1147
- const state = this.readState();
1148
- if (state.pendingBoot !== null) {
1251
+ if (readPendingRootSwap(this.rootDir()) !== null) {
1149
1252
  return {
1150
1253
  accepted: false,
1151
1254
  targetVersion: runningVersion,
1152
1255
  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.`
1256
+ message: "Refused: a version is staged and awaiting restart \u2014 apply it instead of a plain restart."
1154
1257
  };
1155
1258
  }
1156
1259
  this.logger.info("plain node restart requested", {
@@ -1168,118 +1271,37 @@ var require_dist = __commonJS({
1168
1271
  }
1169
1272
  // ── Boot health confirmation ──────────────────────────────────────────
1170
1273
  /**
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.
1274
+ * Called by the post-boot path once the node is READY. In the single-copy
1275
+ * model there is no pending version to promote (the swap already happened in
1276
+ * the starter). This becomes a best-effort GC of orphaned transient dirs
1277
+ * (`.staging-*` / `.trash-*` left by a crash mid-swap) + the dev-uploads
1278
+ * sweep. Kept returning the legacy `{ promoted }` shape for the callers.
1177
1279
  */
1178
1280
  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"));
1281
+ sweepTransientRootDirs(this.rootDir(), this.now(), STALE_TRANSIENT_MS);
1201
1282
  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
1283
  return {
1209
- promoted: promoted.currentVersion
1284
+ promoted: null
1210
1285
  };
1211
1286
  }
1212
1287
  /**
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
1288
  * Promote-time sweep of `server-root/dev-uploads/`: keep only the
1264
1289
  * `keepCount` most recent entries (ordered by the self-describing
1265
1290
  * `-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
1291
  */
1270
1292
  sweepDevUploads(keepCount) {
1271
1293
  const dir = devUploadsDir(this.rootDir());
1272
1294
  let entries;
1273
1295
  try {
1274
- entries = fs5.readdirSync(dir);
1296
+ entries = fs6.readdirSync(dir);
1275
1297
  } catch {
1276
1298
  return;
1277
1299
  }
1278
- const graveyard = path5.join(dir, ".sweeping");
1300
+ const graveyard = path6.join(dir, ".sweeping");
1279
1301
  try {
1280
- for (const residue of fs5.readdirSync(graveyard)) {
1302
+ for (const residue of fs6.readdirSync(graveyard)) {
1281
1303
  try {
1282
- fs5.rmSync(path5.join(graveyard, residue), {
1304
+ fs6.rmSync(path6.join(graveyard, residue), {
1283
1305
  recursive: true,
1284
1306
  force: true
1285
1307
  });
@@ -1294,13 +1316,13 @@ var require_dist = __commonJS({
1294
1316
  ].sort((a, b) => (devChannelEpoch(b) ?? -1) - (devChannelEpoch(a) ?? -1));
1295
1317
  const doomed = byNewestFirst.slice(keepCount);
1296
1318
  if (doomed.length === 0) return;
1297
- fs5.mkdirSync(graveyard, {
1319
+ fs6.mkdirSync(graveyard, {
1298
1320
  recursive: true
1299
1321
  });
1300
1322
  for (const entry of doomed) {
1301
- const aside = path5.join(graveyard, `${entry}-${this.now()}`);
1323
+ const aside = path6.join(graveyard, `${entry}-${this.now()}`);
1302
1324
  try {
1303
- fs5.renameSync(path5.join(dir, entry), aside);
1325
+ fs6.renameSync(path6.join(dir, entry), aside);
1304
1326
  } catch (err) {
1305
1327
  this.logger.warn("failed to move dev-uploads entry aside for sweep", {
1306
1328
  meta: {
@@ -1311,7 +1333,7 @@ var require_dist = __commonJS({
1311
1333
  continue;
1312
1334
  }
1313
1335
  try {
1314
- fs5.rmSync(aside, {
1336
+ fs6.rmSync(aside, {
1315
1337
  recursive: true,
1316
1338
  force: true
1317
1339
  });
@@ -1324,8 +1346,6 @@ var require_dist = __commonJS({
1324
1346
  }
1325
1347
  }
1326
1348
  }
1327
- /** Transient (.staging- / .evicted-) dirs younger than this are never pruned. */
1328
- static STALE_TRANSIENT_MS = 24 * 60 * 60 * 1e3;
1329
1349
  };
1330
1350
  }
1331
1351
  });