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