@camstack/agent 1.1.57 → 1.1.59

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