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