@camstack/agent 1.1.57 → 1.1.58

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,225 @@ 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"));
316
+ var CURRENT_DIRNAME = "current";
317
+ var PENDING_ROOT_SWAP_FILE = ".pending-root-swap.json";
318
+ var LEGACY_FRAMEWORK_MARKERS = [
319
+ ".pending-framework-swap.json",
320
+ ".framework-swap-confirm.json"
321
+ ];
322
+ function currentDir(rootDir) {
323
+ return path3.join(rootDir, CURRENT_DIRNAME);
324
+ }
325
+ function currentEntryPath(rootDir, spec) {
326
+ return rootEntryPath(currentDir(rootDir), spec);
327
+ }
328
+ function stagingDirPath(rootDir, target, pid, now) {
329
+ return path3.join(rootDir, `.staging-${target}-${pid}-${now}`);
330
+ }
331
+ function pendingRootSwapPath(rootDir) {
332
+ return path3.join(rootDir, PENDING_ROOT_SWAP_FILE);
333
+ }
334
+ function isPendingRootSwap(v) {
335
+ if (typeof v !== "object" || v === null) return false;
336
+ const m = v;
337
+ return m["schemaVersion"] === 1 && typeof m["version"] === "string" && typeof m["stagingPath"] === "string" && typeof m["requestedAtMs"] === "number";
338
+ }
339
+ function readPendingRootSwap(rootDir) {
340
+ try {
341
+ const raw = JSON.parse(fs3.readFileSync(pendingRootSwapPath(rootDir), "utf-8"));
342
+ return isPendingRootSwap(raw) ? raw : null;
343
+ } catch {
344
+ return null;
345
+ }
346
+ }
347
+ function writePendingRootSwap(rootDir, marker) {
348
+ fs3.mkdirSync(rootDir, { recursive: true });
349
+ const target = pendingRootSwapPath(rootDir);
350
+ const tmp = `${target}.tmp`;
351
+ fs3.writeFileSync(tmp, JSON.stringify(marker, null, 2), "utf-8");
352
+ fs3.renameSync(tmp, target);
353
+ }
354
+ function clearPendingRootSwap(rootDir) {
355
+ try {
356
+ fs3.rmSync(pendingRootSwapPath(rootDir), { force: true });
357
+ } catch {
358
+ }
359
+ }
360
+ var REQUIRED_NATIVE_PACKAGES = ["better-sqlite3", "sharp", "node-av"];
361
+ var NATIVE_SEARCH_DIRS = {
362
+ "better-sqlite3": ["better-sqlite3"],
363
+ sharp: ["sharp", "@img"],
364
+ "node-av": ["node-av", "@seydx"]
365
+ };
366
+ function hasDotNode(dir, maxDepth) {
367
+ let entries;
368
+ try {
369
+ entries = fs3.readdirSync(dir, { withFileTypes: true });
370
+ } catch {
371
+ return false;
372
+ }
373
+ for (const entry of entries) {
374
+ if (entry.isFile() && entry.name.endsWith(".node")) return true;
375
+ }
376
+ if (maxDepth <= 0) return false;
377
+ for (const entry of entries) {
378
+ if (entry.isDirectory() && hasDotNode(path3.join(dir, entry.name), maxDepth - 1)) return true;
379
+ }
380
+ return false;
381
+ }
382
+ function findMissingNativePrebuilds(closureDir) {
383
+ const nm = path3.join(closureDir, "node_modules");
384
+ const missing = [];
385
+ for (const pkg of REQUIRED_NATIVE_PACKAGES) {
386
+ const searchDirs = NATIVE_SEARCH_DIRS[pkg] ?? [pkg];
387
+ const found = searchDirs.some((rel) => hasDotNode(path3.join(nm, ...rel.split("/")), 4));
388
+ if (!found) missing.push(pkg);
389
+ }
390
+ return missing;
391
+ }
392
+ function rmrf(target) {
393
+ try {
394
+ fs3.rmSync(target, { recursive: true, force: true });
395
+ } catch {
396
+ }
397
+ }
398
+ function errMsg(err) {
399
+ return err instanceof Error ? err.message : String(err);
400
+ }
401
+ function applyPendingRootSwap(rootDir, spec, nodeMajor, now, log) {
402
+ const marker = readPendingRootSwap(rootDir);
403
+ if (marker === null) return { applied: false, version: null, reason: "no pending swap" };
404
+ const staging = marker.stagingPath;
405
+ const dest = currentDir(rootDir);
406
+ const invalid = validateClosureDir(staging, nodeMajor, spec, marker.version);
407
+ if (invalid !== null) {
408
+ if (!fs3.existsSync(staging) && validateClosureDir(dest, nodeMajor, spec, marker.version) === null) {
409
+ clearPendingRootSwap(rootDir);
410
+ log(`[single-copy] pending swap ${marker.version} already applied \u2014 cleared marker`);
411
+ return { applied: true, version: marker.version, reason: "already applied" };
412
+ }
413
+ log(`[single-copy] staged swap ${marker.version} invalid \u2014 discarding (${invalid})`);
414
+ rmrf(staging);
415
+ clearPendingRootSwap(rootDir);
416
+ return { applied: false, version: null, reason: invalid };
417
+ }
418
+ const missing = findMissingNativePrebuilds(staging);
419
+ if (missing.length > 0) {
420
+ log(
421
+ `[single-copy] staged swap ${marker.version} missing native prebuilds: ${missing.join(", ")} \u2014 discarding`
422
+ );
423
+ rmrf(staging);
424
+ clearPendingRootSwap(rootDir);
425
+ return {
426
+ applied: false,
427
+ version: null,
428
+ reason: `missing native prebuilds: ${missing.join(", ")}`
429
+ };
430
+ }
431
+ let trash = null;
432
+ try {
433
+ if (fs3.existsSync(dest)) {
434
+ trash = path3.join(rootDir, `.trash-${now()}-${process.pid}`);
435
+ fs3.renameSync(dest, trash);
436
+ }
437
+ try {
438
+ fs3.renameSync(staging, dest);
439
+ } catch (err) {
440
+ if (trash !== null && !fs3.existsSync(dest)) {
441
+ try {
442
+ fs3.renameSync(trash, dest);
443
+ } catch {
444
+ }
445
+ }
446
+ throw err;
447
+ }
448
+ } catch (err) {
449
+ log(`[single-copy] swap of ${marker.version} FAILED \u2014 current preserved (${errMsg(err)})`);
450
+ return { applied: false, version: null, reason: errMsg(err) };
451
+ }
452
+ if (trash !== null) rmrf(trash);
453
+ clearPendingRootSwap(rootDir);
454
+ log(`[single-copy] applied staged root swap \u2192 ${marker.version}`);
455
+ return { applied: true, version: marker.version, reason: "applied" };
456
+ }
457
+ function migrateToSingleCopy(rootDir, spec, nodeMajor, now, log) {
458
+ const dest = currentDir(rootDir);
459
+ if (fs3.existsSync(dest) && validateClosureDir(dest, nodeMajor, spec) === null) {
460
+ return { migrated: false, version: null, reason: "current already present" };
461
+ }
462
+ const candidates = [];
463
+ const legacyState = readServerRootState(rootDir);
464
+ if (legacyState !== null) {
465
+ for (const v of [
466
+ legacyState.currentVersion,
467
+ legacyState.pendingBoot?.version ?? null,
468
+ legacyState.previousVersion
469
+ ]) {
470
+ if (typeof v === "string" && !candidates.includes(v)) candidates.push(v);
471
+ }
472
+ }
473
+ try {
474
+ for (const entry of fs3.readdirSync(versionsDir(rootDir))) {
475
+ if (!entry.startsWith(".") && !candidates.includes(entry)) candidates.push(entry);
476
+ }
477
+ } catch {
478
+ }
479
+ const source = candidates.find(
480
+ (v) => validateClosureDir(versionDir(rootDir, v), nodeMajor, spec, v) === null
481
+ );
482
+ if (source === void 0) {
483
+ return { migrated: false, version: null, reason: "no valid legacy versions/<X> to adopt" };
484
+ }
485
+ if (fs3.existsSync(dest)) {
486
+ const aside = path3.join(rootDir, `.trash-${now()}-${process.pid}-stale-current`);
487
+ try {
488
+ fs3.renameSync(dest, aside);
489
+ } catch {
490
+ rmrf(dest);
491
+ }
492
+ }
493
+ try {
494
+ fs3.renameSync(versionDir(rootDir, source), dest);
495
+ } catch (err) {
496
+ log(`[single-copy] migration rename of versions/${source} failed (${errMsg(err)})`);
497
+ return { migrated: false, version: null, reason: errMsg(err) };
498
+ }
499
+ rmrf(versionsDir(rootDir));
500
+ rmrf(stateFilePath(rootDir));
501
+ const dataDir = path3.dirname(rootDir);
502
+ for (const marker of LEGACY_FRAMEWORK_MARKERS) rmrf(path3.join(dataDir, marker));
503
+ log(`[single-copy] migrated legacy versions/${source} \u2192 current`);
504
+ return { migrated: true, version: source, reason: "adopted legacy version" };
505
+ }
506
+ function sweepTransientRootDirs(rootDir, now, staleMs) {
507
+ let entries;
508
+ try {
509
+ entries = fs3.readdirSync(rootDir);
510
+ } catch {
511
+ return;
512
+ }
513
+ for (const entry of entries) {
514
+ if (!entry.startsWith(".staging-") && !entry.startsWith(".trash-")) continue;
515
+ const full = path3.join(rootDir, entry);
516
+ try {
517
+ if (now - fs3.statSync(full).mtimeMs < staleMs) continue;
518
+ } catch {
519
+ continue;
520
+ }
521
+ rmrf(full);
522
+ }
523
+ }
524
+ function planBoot(currentValid) {
525
+ if (currentValid) {
526
+ return { kind: "current", reason: "single-copy closure present and valid" };
527
+ }
528
+ return { kind: "baked", reason: "no valid single-copy closure \u2014 booting the baked seed" };
529
+ }
298
530
  function parse(version) {
299
531
  const dashIdx = version.indexOf("-");
300
532
  const base = dashIdx === -1 ? version : version.slice(0, dashIdx);
@@ -322,133 +554,26 @@ var require_dist = __commonJS({
322
554
  if (pa.prerelease > pb.prerelease) return 1;
323
555
  return 0;
324
556
  }
325
- function planBoot(state, isValidVersion, now, seedVersion = null) {
326
- if (state === null) {
327
- return { kind: "baked", reason: "no server-root state", stateToWrite: null };
328
- }
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
- }
368
- }
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
- };
410
- }
411
- 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"
422
- }
423
- }
424
- };
425
- }
426
- return {
427
- kind: "baked",
428
- reason: "no active data-root version",
429
- stateToWrite: changed ? next : null
430
- };
431
- }
432
- var fs3 = __toESM2(require("fs"));
433
- var path3 = __toESM2(require("path"));
557
+ var fs4 = __toESM2(require("fs"));
558
+ var path4 = __toESM2(require("path"));
434
559
  function detectWorkspaceRoot(fromDir) {
435
- let dir = path3.resolve(fromDir);
560
+ let dir = path4.resolve(fromDir);
436
561
  for (; ; ) {
437
- const pkgPath = path3.join(dir, "package.json");
562
+ const pkgPath = path4.join(dir, "package.json");
438
563
  try {
439
- const raw = JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
564
+ const raw = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
440
565
  if (typeof raw === "object" && raw !== null && raw["workspaces"] !== void 0) {
441
566
  return dir;
442
567
  }
443
568
  } catch {
444
569
  }
445
- const parent = path3.dirname(dir);
570
+ const parent = path4.dirname(dir);
446
571
  if (parent === dir) return null;
447
572
  dir = parent;
448
573
  }
449
574
  }
450
- var fs4 = __toESM2(require("fs"));
451
- var path4 = __toESM2(require("path"));
575
+ var fs5 = __toESM2(require("fs"));
576
+ var path5 = __toESM2(require("path"));
452
577
  var import_node_url = require("url");
453
578
  var HOST_EXTERNAL_SPECIFIERS = [
454
579
  "@camstack/system",
@@ -476,7 +601,7 @@ var require_dist = __commonJS({
476
601
  return;
477
602
  }
478
603
  const anchorURL = (0, import_node_url.pathToFileURL)(
479
- path4.join(activeRootDir, "node_modules", "__camstack_starter_anchor__.js")
604
+ path5.join(activeRootDir, "node_modules", "__camstack_starter_anchor__.js")
480
605
  ).href;
481
606
  const hooks = {
482
607
  resolve: (specifier, context, nextResolve) => {
@@ -491,23 +616,24 @@ var require_dist = __commonJS({
491
616
  };
492
617
  registerHooks(hooks);
493
618
  }
494
- function readSeedVersion(seedEntry) {
619
+ function readClosureVersion(closureDir, spec) {
495
620
  try {
496
- const pkgJsonPath = path4.join(path4.dirname(seedEntry), "..", "package.json");
497
- const raw = JSON.parse(fs4.readFileSync(pkgJsonPath, "utf-8"));
621
+ const raw = JSON.parse(
622
+ fs5.readFileSync(path5.join(rootPackageDir(closureDir, spec), "package.json"), "utf-8")
623
+ );
498
624
  if (typeof raw === "object" && raw !== null) {
499
625
  const version = raw["version"];
500
626
  if (typeof version === "string") return version;
501
627
  }
502
- return null;
503
628
  } catch {
504
- return null;
505
629
  }
630
+ return null;
506
631
  }
507
632
  function runNodeStarter2(options) {
508
633
  const env = options.env ?? process.env;
509
634
  const now = options.now ?? Date.now;
510
635
  const registerResolver = options.registerResolver ?? registerActiveRootResolver;
636
+ const nodeMajor = options.nodeMajor ?? Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
511
637
  const disabled = env[options.envNames.killSwitch] === "off";
512
638
  const workspaceRoot = detectWorkspaceRoot(options.starterDir);
513
639
  if (workspaceRoot !== null) {
@@ -525,40 +651,34 @@ var require_dist = __commonJS({
525
651
  return;
526
652
  }
527
653
  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
- }
654
+ const swap = applyPendingRootSwap(rootDir, options.spec, nodeMajor, now, (m) => console.log(m));
655
+ if (swap.applied) console.log(`[starter] applied staged update \u2192 ${swap.version ?? "?"}`);
656
+ else if (swap.reason !== "no pending swap")
657
+ console.warn(`[starter] staged update NOT applied: ${swap.reason}`);
658
+ const migration = migrateToSingleCopy(
659
+ rootDir,
660
+ options.spec,
661
+ nodeMajor,
662
+ now,
663
+ (m) => console.log(m)
664
+ );
665
+ if (migration.migrated)
666
+ console.log(`[starter] migrated legacy layout \u2192 single copy (${migration.version ?? "?"})`);
667
+ const activeRootDir = currentDir(rootDir);
668
+ const invalid = validateClosureDir(activeRootDir, nodeMajor, options.spec);
669
+ if (invalid !== null && fs5.existsSync(activeRootDir)) {
670
+ console.warn(`[starter] single copy invalid: ${invalid}`);
552
671
  }
553
- if (plan.kind === "data-root") {
554
- const activeRootDir = versionDir(rootDir, plan.version);
555
- const entry = rootEntryPath(activeRootDir, options.spec);
672
+ const plan = planBoot(invalid === null);
673
+ if (plan.kind === "current") {
674
+ const version = readClosureVersion(activeRootDir, options.spec);
675
+ const entry = currentEntryPath(rootDir, options.spec);
556
676
  console.log(
557
- `[starter] booting ${options.spec.packageName}@${plan.version} from ${activeRootDir}` + (plan.probation ? " (probation boot \u2014 health-check armed)" : "")
677
+ `[starter] booting ${options.spec.packageName}@${version ?? "?"} from ${activeRootDir}`
558
678
  );
559
679
  env[options.envNames.bootMode] = "data-root";
560
680
  env[options.envNames.activeRoot] = activeRootDir;
561
- env[options.envNames.activeVersion] = plan.version;
681
+ if (version !== null) env[options.envNames.activeVersion] = version;
562
682
  registerResolver(activeRootDir);
563
683
  options.loadEntry(entry);
564
684
  return;
@@ -568,8 +688,8 @@ var require_dist = __commonJS({
568
688
  options.loadEntry(options.seedEntry);
569
689
  }
570
690
  var import_node_child_process = require("child_process");
571
- var fs5 = __toESM2(require("fs"));
572
- var path5 = __toESM2(require("path"));
691
+ var fs6 = __toESM2(require("fs"));
692
+ var path6 = __toESM2(require("path"));
573
693
  var import_node_util = require("util");
574
694
  var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
575
695
  function buildNpmRegistryArgs(registry) {
@@ -579,9 +699,10 @@ var require_dist = __commonJS({
579
699
  var NPM_VIEW_TIMEOUT_MS = 2e4;
580
700
  var NPM_INSTALL_TIMEOUT_MS = 15 * 6e4;
581
701
  var RESTART_REASON_PREFIX = "server-update";
702
+ var STALE_TRANSIENT_MS = 24 * 60 * 60 * 1e3;
582
703
  function readPackageVersion(pkgJsonPath) {
583
704
  try {
584
- const raw = JSON.parse(fs5.readFileSync(pkgJsonPath, "utf-8"));
705
+ const raw = JSON.parse(fs6.readFileSync(pkgJsonPath, "utf-8"));
585
706
  if (typeof raw === "object" && raw !== null) {
586
707
  const version = raw["version"];
587
708
  if (typeof version === "string") return version;
@@ -590,7 +711,7 @@ var require_dist = __commonJS({
590
711
  }
591
712
  return null;
592
713
  }
593
- var RootUpdateService = class _RootUpdateService {
714
+ var RootUpdateService = class {
594
715
  spec;
595
716
  envNames;
596
717
  logger;
@@ -608,7 +729,7 @@ var require_dist = __commonJS({
608
729
  this.envNames = options.envNames;
609
730
  this.logger = options.logger;
610
731
  this.restartServerFn = options.restartServer;
611
- this.dataDir = path5.resolve(options.dataDir);
732
+ this.dataDir = path6.resolve(options.dataDir);
612
733
  this.execNpm = options.execNpm ?? (async (args, opts) => {
613
734
  const { stdout } = await execFileAsync("npm", [...args], {
614
735
  cwd: opts.cwd,
@@ -637,57 +758,37 @@ var require_dist = __commonJS({
637
758
  seedVersion() {
638
759
  const seedDir = this.env[this.envNames.seedDir];
639
760
  if (seedDir === void 0 || seedDir.length === 0) return null;
640
- return readPackageVersion(path5.join(seedDir, "package.json"));
761
+ return readPackageVersion(path6.join(seedDir, "package.json"));
641
762
  }
642
763
  rootDir() {
643
764
  return serverRootDir(this.dataDir);
644
765
  }
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) {
766
+ updateState() {
661
767
  if (this.inFlight === "checking") return "checking";
662
768
  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
- }
769
+ if (readPendingRootSwap(this.rootDir()) !== null) return "pending-restart";
670
770
  return "idle";
671
771
  }
672
772
  async getServerPackageStatus() {
673
- const { state, corrupt } = this.readStateInfo();
674
773
  const runningVersion = this.runningVersion();
675
774
  const latestVersion = this.checkCache?.latestVersion ?? null;
775
+ const pending = readPendingRootSwap(this.rootDir());
676
776
  const updateAvailable = latestVersion !== null && runningVersion !== null && compareSemver(latestVersion, runningVersion) > 0;
677
777
  return {
678
778
  packageName: this.spec.packageName,
679
779
  runningVersion,
680
780
  nodeRuntimeVersion: process.versions.node,
681
- activeVersion: this.env[this.envNames.activeVersion] ?? state.currentVersion,
682
- previousVersion: state.previousVersion,
781
+ activeVersion: this.env[this.envNames.activeVersion] ?? runningVersion,
782
+ // Single-copy model: no N-1 retention, no rollback record.
783
+ previousVersion: null,
683
784
  seedVersion: this.seedVersion(),
684
785
  latestVersion,
685
786
  updateAvailable,
686
787
  bootMode: this.resolveBootMode(),
687
- updateState: this.updateState(state),
688
- pendingVersion: state.pendingBoot?.version ?? null,
689
- rolledBack: state.rolledBack,
690
- stateFileCorrupt: corrupt,
788
+ updateState: this.updateState(),
789
+ pendingVersion: pending?.version ?? null,
790
+ rolledBack: null,
791
+ stateFileCorrupt: false,
691
792
  lastCheckedAtMs: this.checkCache?.checkedAtMs ?? null
692
793
  };
693
794
  }
@@ -764,13 +865,13 @@ var require_dist = __commonJS({
764
865
  message: `Refused: another operation is in flight (${this.inFlight}).`
765
866
  };
766
867
  }
767
- const stateBefore = this.readState();
768
- if (stateBefore.pendingBoot !== null) {
868
+ const pendingBefore = readPendingRootSwap(this.rootDir());
869
+ if (pendingBefore !== null) {
769
870
  return {
770
871
  accepted: false,
771
872
  targetVersion: input.version ?? null,
772
873
  restarting: false,
773
- message: `Refused: version ${stateBefore.pendingBoot.version} is already staged and awaiting restart.`
874
+ message: `Refused: version ${pendingBefore.version} is already staged and awaiting restart.`
774
875
  };
775
876
  }
776
877
  let target = input.version ?? null;
@@ -797,7 +898,7 @@ var require_dist = __commonJS({
797
898
  }
798
899
  if (isDevChannelVersion(target)) {
799
900
  const uploadsDir = devUploadVersionDir(this.rootDir(), target);
800
- if (!fs5.existsSync(uploadsDir)) {
901
+ if (!fs6.existsSync(uploadsDir)) {
801
902
  return {
802
903
  accepted: false,
803
904
  targetVersion: target,
@@ -807,8 +908,9 @@ var require_dist = __commonJS({
807
908
  }
808
909
  }
809
910
  this.inFlight = "staging";
911
+ let stagingPath;
810
912
  try {
811
- await this.stageAndActivate(target);
913
+ stagingPath = await this.stageClosure(target);
812
914
  } catch (err) {
813
915
  const message = err instanceof Error ? err.message : String(err);
814
916
  this.logger.error("root package update staging failed", {
@@ -823,45 +925,42 @@ var require_dist = __commonJS({
823
925
  } finally {
824
926
  this.inFlight = "idle";
825
927
  }
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
928
+ writePendingRootSwap(this.rootDir(), {
929
+ schemaVersion: 1,
930
+ version: target,
931
+ stagingPath,
932
+ requestedAtMs: this.now()
836
933
  });
837
934
  this.logger.info("root package update staged \u2014 restarting to apply", {
838
- meta: { targetVersion: target, fromVersion: state.currentVersion }
935
+ meta: { targetVersion: target, fromVersion: runningVersion }
839
936
  });
840
937
  this.restartServerFn(`${RESTART_REASON_PREFIX}: ${this.spec.packageName}@${target}`);
841
938
  return {
842
939
  accepted: true,
843
940
  targetVersion: target,
844
941
  restarting: true,
845
- message: `Staged ${this.spec.packageName}@${target} \u2014 restarting to apply (probation boot with auto-rollback).`
942
+ message: `Staged ${this.spec.packageName}@${target} \u2014 restarting to apply (single automatic reboot).`
846
943
  };
847
944
  }
848
945
  /**
849
- * npm-install the target closure into a SAME-FS staging dir inside
850
- * `versions/`, validate it, then atomically rename it into place.
946
+ * npm-install the target closure into a SAME-FS `.staging-*` dir, then
947
+ * validate it (entry + version + native prebuilds). Returns the staging dir
948
+ * path for the pending-root-swap marker; the STARTER swaps it into `current`
949
+ * on the next boot. Throws (cleaning up the staging dir) on any failure so
950
+ * `applyServerUpdate` reports a staging error and never arms a bad swap.
851
951
  */
852
- async stageAndActivate(target) {
952
+ async stageClosure(target) {
853
953
  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 });
954
+ fs6.mkdirSync(rootDir, { recursive: true });
955
+ const stagingDir = stagingDirPath(rootDir, target, process.pid, this.now());
956
+ fs6.mkdirSync(stagingDir, { recursive: true });
858
957
  try {
859
958
  const devDir = devUploadVersionDir(rootDir, target);
860
959
  const registry = this.env["CAMSTACK_NPM_REGISTRY"];
861
960
  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"),
961
+ if (fs6.existsSync(devDir)) {
962
+ fs6.writeFileSync(
963
+ path6.join(stagingDir, "package.json"),
865
964
  JSON.stringify(this.buildDevUploadsPackageJson(devDir, target), null, 2),
866
965
  "utf-8"
867
966
  );
@@ -870,8 +969,8 @@ var require_dist = __commonJS({
870
969
  timeoutMs: NPM_INSTALL_TIMEOUT_MS
871
970
  });
872
971
  } else {
873
- fs5.writeFileSync(
874
- path5.join(stagingDir, "package.json"),
972
+ fs6.writeFileSync(
973
+ path6.join(stagingDir, "package.json"),
875
974
  JSON.stringify({ name: "camstack-node-root", private: true }, null, 2),
876
975
  "utf-8"
877
976
  );
@@ -881,37 +980,32 @@ var require_dist = __commonJS({
881
980
  );
882
981
  }
883
982
  const entry = rootEntryPath(stagingDir, this.spec);
884
- if (!fs5.existsSync(entry)) {
983
+ if (!fs6.existsSync(entry)) {
885
984
  throw new Error(`staged closure is missing the root entry (${entry})`);
886
985
  }
887
- const stagedVersion = readPackageVersion(
888
- path5.join(rootPackageDir(stagingDir, this.spec), "package.json")
889
- );
890
- if (stagedVersion !== target) {
986
+ const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
987
+ const invalid = validateClosureDir(stagingDir, nodeMajor, this.spec, target);
988
+ if (invalid !== null) {
989
+ throw new Error(`staged closure invalid: ${invalid}`);
990
+ }
991
+ const missingNatives = findMissingNativePrebuilds(stagingDir);
992
+ if (missingNatives.length > 0) {
891
993
  throw new Error(
892
- `staged closure version mismatch: expected ${target}, got ${stagedVersion ?? "unknown"}`
994
+ `staged closure missing native prebuilds: ${missingNatives.join(", ")} \u2014 refusing to arm a swap that would break the forked runners`
893
995
  );
894
996
  }
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);
997
+ return stagingDir;
902
998
  } catch (err) {
903
- await fs5.promises.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
999
+ await fs6.promises.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
904
1000
  throw err;
905
1001
  }
906
1002
  }
907
1003
  /**
908
1004
  * Synthetic staging package.json for the dev server-deploy channel:
909
1005
  * `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.
1006
+ * tgz; `overrides` maps EVERY OTHER uploaded `@camstack/<name>` to its tgz.
912
1007
  * 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).
1008
+ * version, a missing root package, or a listed-but-absent tgz all throw.
915
1009
  */
916
1010
  buildDevUploadsPackageJson(devDir, target) {
917
1011
  const manifest = readDevUploadManifest(devDir);
@@ -930,8 +1024,8 @@ var require_dist = __commonJS({
930
1024
  );
931
1025
  }
932
1026
  const fileRef = (filename) => {
933
- const abs = path5.join(devDir, filename);
934
- if (!fs5.existsSync(abs)) {
1027
+ const abs = path6.join(devDir, filename);
1028
+ if (!fs6.existsSync(abs)) {
935
1029
  throw new Error(`dev-uploads tarball listed in the manifest is missing: ${abs}`);
936
1030
  }
937
1031
  return `file:${abs}`;
@@ -948,77 +1042,28 @@ var require_dist = __commonJS({
948
1042
  ...Object.keys(overrides).length > 0 ? { overrides } : {}
949
1043
  };
950
1044
  }
951
- // ── Rollback ──────────────────────────────────────────────────────────
1045
+ // ── Rollback (removed — single-copy has no N-1) ────────────────────────
1046
+ /**
1047
+ * Rollback is NOT supported in the single-copy model — there is no retained
1048
+ * N-1 copy to revert to (operator's accepted tradeoff). Recovery is manual:
1049
+ * re-apply a known-good version via {@link applyServerUpdate}, or reinstall
1050
+ * the image seed. Kept on the surface so the cap contract is unchanged.
1051
+ */
952
1052
  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
1053
  return {
1009
- accepted: true,
1010
- targetVersion: state.previousVersion,
1011
- restarting: true,
1012
- message: `Rolling back to ${this.spec.packageName}@${state.previousVersion} \u2014 restarting.`
1054
+ accepted: false,
1055
+ targetVersion: null,
1056
+ restarting: false,
1057
+ 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
1058
  };
1014
1059
  }
1015
1060
  // ── Plain restart ─────────────────────────────────────────────────────
1016
1061
  /**
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).
1062
+ * Plain process restart — no version change. Refuse while a stage is in
1063
+ * flight (a concurrent npm install must not be interrupted) or while a
1064
+ * version is already staged awaiting restart (a naive bounce would boot the
1065
+ * OLD version and orphan the pending swap the operator should apply
1066
+ * instead).
1022
1067
  */
1023
1068
  restartNode() {
1024
1069
  const runningVersion = this.runningVersion();
@@ -1030,13 +1075,12 @@ var require_dist = __commonJS({
1030
1075
  message: `Refused: another operation is in flight (${this.inFlight}).`
1031
1076
  };
1032
1077
  }
1033
- const state = this.readState();
1034
- if (state.pendingBoot !== null) {
1078
+ if (readPendingRootSwap(this.rootDir()) !== null) {
1035
1079
  return {
1036
1080
  accepted: false,
1037
1081
  targetVersion: runningVersion,
1038
1082
  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.`
1083
+ message: "Refused: a version is staged and awaiting restart \u2014 apply it instead of a plain restart."
1040
1084
  };
1041
1085
  }
1042
1086
  this.logger.info("plain node restart requested", { meta: { runningVersion } });
@@ -1050,103 +1094,35 @@ var require_dist = __commonJS({
1050
1094
  }
1051
1095
  // ── Boot health confirmation ──────────────────────────────────────────
1052
1096
  /**
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.
1097
+ * Called by the post-boot path once the node is READY. In the single-copy
1098
+ * model there is no pending version to promote (the swap already happened in
1099
+ * the starter). This becomes a best-effort GC of orphaned transient dirs
1100
+ * (`.staging-*` / `.trash-*` left by a crash mid-swap) + the dev-uploads
1101
+ * sweep. Kept returning the legacy `{ promoted }` shape for the callers.
1059
1102
  */
1060
1103
  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
- );
1104
+ sweepTransientRootDirs(this.rootDir(), this.now(), STALE_TRANSIENT_MS);
1080
1105
  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
- }
1106
+ return { promoted: null };
1128
1107
  }
1129
1108
  /**
1130
1109
  * Promote-time sweep of `server-root/dev-uploads/`: keep only the
1131
1110
  * `keepCount` most recent entries (ordered by the self-describing
1132
1111
  * `-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
1112
  */
1137
1113
  sweepDevUploads(keepCount) {
1138
1114
  const dir = devUploadsDir(this.rootDir());
1139
1115
  let entries;
1140
1116
  try {
1141
- entries = fs5.readdirSync(dir);
1117
+ entries = fs6.readdirSync(dir);
1142
1118
  } catch {
1143
1119
  return;
1144
1120
  }
1145
- const graveyard = path5.join(dir, ".sweeping");
1121
+ const graveyard = path6.join(dir, ".sweeping");
1146
1122
  try {
1147
- for (const residue of fs5.readdirSync(graveyard)) {
1123
+ for (const residue of fs6.readdirSync(graveyard)) {
1148
1124
  try {
1149
- fs5.rmSync(path5.join(graveyard, residue), { recursive: true, force: true });
1125
+ fs6.rmSync(path6.join(graveyard, residue), { recursive: true, force: true });
1150
1126
  } catch {
1151
1127
  }
1152
1128
  }
@@ -1158,11 +1134,11 @@ var require_dist = __commonJS({
1158
1134
  );
1159
1135
  const doomed = byNewestFirst.slice(keepCount);
1160
1136
  if (doomed.length === 0) return;
1161
- fs5.mkdirSync(graveyard, { recursive: true });
1137
+ fs6.mkdirSync(graveyard, { recursive: true });
1162
1138
  for (const entry of doomed) {
1163
- const aside = path5.join(graveyard, `${entry}-${this.now()}`);
1139
+ const aside = path6.join(graveyard, `${entry}-${this.now()}`);
1164
1140
  try {
1165
- fs5.renameSync(path5.join(dir, entry), aside);
1141
+ fs6.renameSync(path6.join(dir, entry), aside);
1166
1142
  } catch (err) {
1167
1143
  this.logger.warn("failed to move dev-uploads entry aside for sweep", {
1168
1144
  meta: { entry, error: err instanceof Error ? err.message : String(err) }
@@ -1170,14 +1146,12 @@ var require_dist = __commonJS({
1170
1146
  continue;
1171
1147
  }
1172
1148
  try {
1173
- fs5.rmSync(aside, { recursive: true, force: true });
1149
+ fs6.rmSync(aside, { recursive: true, force: true });
1174
1150
  this.logger.debug("swept dev-uploads entry", { meta: { entry } });
1175
1151
  } catch {
1176
1152
  }
1177
1153
  }
1178
1154
  }
1179
- /** Transient (.staging- / .evicted-) dirs younger than this are never pruned. */
1180
- static STALE_TRANSIENT_MS = 24 * 60 * 60 * 1e3;
1181
1155
  };
1182
1156
  }
1183
1157
  });