@camstack/server 1.1.70 → 1.1.72
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/api/core/cap-providers.js +0 -11
- package/dist/api/core/lifecycle-job-runner.js +0 -2
- package/dist/api/trpc/generated-cap-routers.js +0 -8
- package/dist/api/trpc/trpc.router.js +49 -2
- package/dist/boot/post-boot.service.js +4 -17
- package/dist/boot/reconcile-lifecycle-jobs.js +6 -5
- package/dist/core/addon/addon-package.service.js +17 -388
- package/dist/core/addon/addon-registry.service.js +26 -17
- package/dist/core/server-update/server-update.service.js +10 -10
- package/dist/launcher.js +22 -19
- package/dist/manual-boot.js +0 -27
- package/dist/server-root/index.js +444 -398
- package/dist/starter.js +3 -3
- package/package.json +8 -8
- package/dist/boot/resume-framework-swap.js +0 -119
- package/dist/core/addon/system-addons-sync.js +0 -121
- package/dist/launcher-framework-swap.js +0 -408
- package/dist/request-framework-swap.js +0 -41
|
@@ -74,31 +74,41 @@ var require_dist = __commonJS({
|
|
|
74
74
|
var src_exports = {};
|
|
75
75
|
__export2(src_exports, {
|
|
76
76
|
AGENT_ROOT_SPEC: /* @__PURE__ */ __name(() => AGENT_ROOT_SPEC, "AGENT_ROOT_SPEC"),
|
|
77
|
+
CURRENT_DIRNAME: /* @__PURE__ */ __name(() => CURRENT_DIRNAME, "CURRENT_DIRNAME"),
|
|
77
78
|
DEV_UPLOADS_DIRNAME: /* @__PURE__ */ __name(() => DEV_UPLOADS_DIRNAME, "DEV_UPLOADS_DIRNAME"),
|
|
78
79
|
DEV_UPLOADS_KEEP_COUNT: /* @__PURE__ */ __name(() => DEV_UPLOADS_KEEP_COUNT, "DEV_UPLOADS_KEEP_COUNT"),
|
|
79
80
|
DEV_UPLOAD_MANIFEST_FILE: /* @__PURE__ */ __name(() => DEV_UPLOAD_MANIFEST_FILE, "DEV_UPLOAD_MANIFEST_FILE"),
|
|
80
81
|
HOST_EXTERNAL_SPECIFIERS: /* @__PURE__ */ __name(() => HOST_EXTERNAL_SPECIFIERS, "HOST_EXTERNAL_SPECIFIERS"),
|
|
81
82
|
HUB_ROOT_SPEC: /* @__PURE__ */ __name(() => HUB_ROOT_SPEC2, "HUB_ROOT_SPEC"),
|
|
83
|
+
PENDING_ROOT_SWAP_FILE: /* @__PURE__ */ __name(() => PENDING_ROOT_SWAP_FILE, "PENDING_ROOT_SWAP_FILE"),
|
|
84
|
+
REQUIRED_NATIVE_PACKAGES: /* @__PURE__ */ __name(() => REQUIRED_NATIVE_PACKAGES, "REQUIRED_NATIVE_PACKAGES"),
|
|
82
85
|
RESTART_INTENT_FILE: /* @__PURE__ */ __name(() => RESTART_INTENT_FILE, "RESTART_INTENT_FILE"),
|
|
83
86
|
RootUpdateService: /* @__PURE__ */ __name(() => RootUpdateService, "RootUpdateService"),
|
|
84
87
|
SERVER_ROOT_DIRNAME: /* @__PURE__ */ __name(() => SERVER_ROOT_DIRNAME, "SERVER_ROOT_DIRNAME"),
|
|
85
88
|
SERVER_ROOT_STATE_FILE: /* @__PURE__ */ __name(() => SERVER_ROOT_STATE_FILE, "SERVER_ROOT_STATE_FILE"),
|
|
89
|
+
applyPendingRootSwap: /* @__PURE__ */ __name(() => applyPendingRootSwap, "applyPendingRootSwap"),
|
|
90
|
+
clearPendingRootSwap: /* @__PURE__ */ __name(() => clearPendingRootSwap, "clearPendingRootSwap"),
|
|
86
91
|
clearRestartIntentMarker: /* @__PURE__ */ __name(() => clearRestartIntentMarker, "clearRestartIntentMarker"),
|
|
87
92
|
compareSemver: /* @__PURE__ */ __name(() => compareSemver, "compareSemver"),
|
|
93
|
+
currentDir: /* @__PURE__ */ __name(() => currentDir, "currentDir"),
|
|
94
|
+
currentEntryPath: /* @__PURE__ */ __name(() => currentEntryPath, "currentEntryPath"),
|
|
88
95
|
detectWorkspaceRoot: /* @__PURE__ */ __name(() => detectWorkspaceRoot, "detectWorkspaceRoot"),
|
|
89
96
|
devChannelEpoch: /* @__PURE__ */ __name(() => devChannelEpoch, "devChannelEpoch"),
|
|
90
97
|
devUploadManifestPath: /* @__PURE__ */ __name(() => devUploadManifestPath, "devUploadManifestPath"),
|
|
91
98
|
devUploadVersionDir: /* @__PURE__ */ __name(() => devUploadVersionDir, "devUploadVersionDir"),
|
|
92
99
|
devUploadsDir: /* @__PURE__ */ __name(() => devUploadsDir, "devUploadsDir"),
|
|
93
100
|
emptyServerRootState: /* @__PURE__ */ __name(() => emptyServerRootState, "emptyServerRootState"),
|
|
101
|
+
findMissingNativePrebuilds: /* @__PURE__ */ __name(() => findMissingNativePrebuilds, "findMissingNativePrebuilds"),
|
|
94
102
|
isDevChannelVersion: /* @__PURE__ */ __name(() => isDevChannelVersion, "isDevChannelVersion"),
|
|
95
103
|
isHostExternal: /* @__PURE__ */ __name(() => isHostExternal, "isHostExternal"),
|
|
96
104
|
isServerRootState: /* @__PURE__ */ __name(() => isServerRootState, "isServerRootState"),
|
|
105
|
+
migrateToSingleCopy: /* @__PURE__ */ __name(() => migrateToSingleCopy, "migrateToSingleCopy"),
|
|
97
106
|
minNodeMajorOf: /* @__PURE__ */ __name(() => minNodeMajorOf, "minNodeMajorOf"),
|
|
107
|
+
pendingRootSwapPath: /* @__PURE__ */ __name(() => pendingRootSwapPath, "pendingRootSwapPath"),
|
|
98
108
|
planBoot: /* @__PURE__ */ __name(() => planBoot, "planBoot"),
|
|
99
109
|
readDevUploadManifest: /* @__PURE__ */ __name(() => readDevUploadManifest, "readDevUploadManifest"),
|
|
110
|
+
readPendingRootSwap: /* @__PURE__ */ __name(() => readPendingRootSwap, "readPendingRootSwap"),
|
|
100
111
|
readRestartIntentMarker: /* @__PURE__ */ __name(() => readRestartIntentMarker, "readRestartIntentMarker"),
|
|
101
|
-
readSeedVersion: /* @__PURE__ */ __name(() => readSeedVersion, "readSeedVersion"),
|
|
102
112
|
readServerRootState: /* @__PURE__ */ __name(() => readServerRootState, "readServerRootState"),
|
|
103
113
|
registerActiveRootResolver: /* @__PURE__ */ __name(() => registerActiveRootResolver, "registerActiveRootResolver"),
|
|
104
114
|
restartIntentMarkerPath: /* @__PURE__ */ __name(() => restartIntentMarkerPath, "restartIntentMarkerPath"),
|
|
@@ -106,11 +116,15 @@ var require_dist = __commonJS({
|
|
|
106
116
|
rootPackageDir: /* @__PURE__ */ __name(() => rootPackageDir2, "rootPackageDir"),
|
|
107
117
|
runNodeStarter: /* @__PURE__ */ __name(() => runNodeStarter, "runNodeStarter"),
|
|
108
118
|
serverRootDir: /* @__PURE__ */ __name(() => serverRootDir, "serverRootDir"),
|
|
119
|
+
stagingDirPath: /* @__PURE__ */ __name(() => stagingDirPath, "stagingDirPath"),
|
|
109
120
|
stateFilePath: /* @__PURE__ */ __name(() => stateFilePath, "stateFilePath"),
|
|
121
|
+
sweepTransientRootDirs: /* @__PURE__ */ __name(() => sweepTransientRootDirs, "sweepTransientRootDirs"),
|
|
122
|
+
validateClosureDir: /* @__PURE__ */ __name(() => validateClosureDir, "validateClosureDir"),
|
|
110
123
|
validateVersionDir: /* @__PURE__ */ __name(() => validateVersionDir, "validateVersionDir"),
|
|
111
124
|
versionDir: /* @__PURE__ */ __name(() => versionDir, "versionDir"),
|
|
112
125
|
versionsDir: /* @__PURE__ */ __name(() => versionsDir, "versionsDir"),
|
|
113
126
|
writeDevUploadManifest: /* @__PURE__ */ __name(() => writeDevUploadManifest, "writeDevUploadManifest"),
|
|
127
|
+
writePendingRootSwap: /* @__PURE__ */ __name(() => writePendingRootSwap, "writePendingRootSwap"),
|
|
114
128
|
writeRestartIntentMarker: /* @__PURE__ */ __name(() => writeRestartIntentMarker, "writeRestartIntentMarker"),
|
|
115
129
|
writeServerRootState: /* @__PURE__ */ __name(() => writeServerRootState, "writeServerRootState")
|
|
116
130
|
});
|
|
@@ -316,13 +330,12 @@ var require_dist = __commonJS({
|
|
|
316
330
|
return Number.isNaN(major) ? null : major;
|
|
317
331
|
}
|
|
318
332
|
__name(minNodeMajorOf, "minNodeMajorOf");
|
|
319
|
-
function
|
|
320
|
-
const
|
|
321
|
-
const entry = rootEntryPath2(vDir, spec);
|
|
333
|
+
function validateClosureDir(closureDir, nodeMajor, spec, expectedVersion) {
|
|
334
|
+
const entry = rootEntryPath2(closureDir, spec);
|
|
322
335
|
if (!fs2.existsSync(entry)) {
|
|
323
336
|
return `root entry missing: ${entry}`;
|
|
324
337
|
}
|
|
325
|
-
const pkgJsonPath = path2.join(rootPackageDir2(
|
|
338
|
+
const pkgJsonPath = path2.join(rootPackageDir2(closureDir, spec), "package.json");
|
|
326
339
|
let pkg;
|
|
327
340
|
try {
|
|
328
341
|
const raw = JSON.parse(fs2.readFileSync(pkgJsonPath, "utf-8"));
|
|
@@ -336,8 +349,8 @@ var require_dist = __commonJS({
|
|
|
336
349
|
if (pkg["name"] !== spec.packageName) {
|
|
337
350
|
return `package name mismatch: expected ${spec.packageName}, got ${String(pkg["name"])}`;
|
|
338
351
|
}
|
|
339
|
-
if (pkg["version"] !==
|
|
340
|
-
return `package version mismatch:
|
|
352
|
+
if (expectedVersion !== void 0 && pkg["version"] !== expectedVersion) {
|
|
353
|
+
return `package version mismatch: expected ${expectedVersion}, package.json says ${String(pkg["version"])}`;
|
|
341
354
|
}
|
|
342
355
|
const engines = pkg["engines"];
|
|
343
356
|
if (typeof engines === "object" && engines !== null) {
|
|
@@ -351,7 +364,13 @@ var require_dist = __commonJS({
|
|
|
351
364
|
}
|
|
352
365
|
return null;
|
|
353
366
|
}
|
|
367
|
+
__name(validateClosureDir, "validateClosureDir");
|
|
368
|
+
function validateVersionDir(rootDir, version, nodeMajor, spec) {
|
|
369
|
+
return validateClosureDir(versionDir(rootDir, version), nodeMajor, spec, version);
|
|
370
|
+
}
|
|
354
371
|
__name(validateVersionDir, "validateVersionDir");
|
|
372
|
+
var fs3 = __toESM2(require("fs"));
|
|
373
|
+
var path3 = __toESM2(require("path"));
|
|
355
374
|
function parse(version) {
|
|
356
375
|
const dashIdx = version.indexOf("-");
|
|
357
376
|
const base = dashIdx === -1 ? version : version.slice(0, dashIdx);
|
|
@@ -384,142 +403,344 @@ var require_dist = __commonJS({
|
|
|
384
403
|
return 0;
|
|
385
404
|
}
|
|
386
405
|
__name(compareSemver, "compareSemver");
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
406
|
+
var CURRENT_DIRNAME = "current";
|
|
407
|
+
var PENDING_ROOT_SWAP_FILE = ".pending-root-swap.json";
|
|
408
|
+
var LEGACY_FRAMEWORK_MARKERS = [
|
|
409
|
+
".pending-framework-swap.json",
|
|
410
|
+
".framework-swap-confirm.json"
|
|
411
|
+
];
|
|
412
|
+
function currentDir(rootDir) {
|
|
413
|
+
return path3.join(rootDir, CURRENT_DIRNAME);
|
|
414
|
+
}
|
|
415
|
+
__name(currentDir, "currentDir");
|
|
416
|
+
function currentEntryPath(rootDir, spec) {
|
|
417
|
+
return rootEntryPath2(currentDir(rootDir), spec);
|
|
418
|
+
}
|
|
419
|
+
__name(currentEntryPath, "currentEntryPath");
|
|
420
|
+
function stagingDirPath(rootDir, target, pid, now) {
|
|
421
|
+
return path3.join(rootDir, `.staging-${target}-${pid}-${now}`);
|
|
422
|
+
}
|
|
423
|
+
__name(stagingDirPath, "stagingDirPath");
|
|
424
|
+
function pendingRootSwapPath(rootDir) {
|
|
425
|
+
return path3.join(rootDir, PENDING_ROOT_SWAP_FILE);
|
|
426
|
+
}
|
|
427
|
+
__name(pendingRootSwapPath, "pendingRootSwapPath");
|
|
428
|
+
function isPendingRootSwap(v) {
|
|
429
|
+
if (typeof v !== "object" || v === null) return false;
|
|
430
|
+
const m = v;
|
|
431
|
+
return m["schemaVersion"] === 1 && typeof m["version"] === "string" && typeof m["stagingPath"] === "string" && typeof m["requestedAtMs"] === "number";
|
|
432
|
+
}
|
|
433
|
+
__name(isPendingRootSwap, "isPendingRootSwap");
|
|
434
|
+
function readPendingRootSwap(rootDir) {
|
|
435
|
+
try {
|
|
436
|
+
const raw = JSON.parse(fs3.readFileSync(pendingRootSwapPath(rootDir), "utf-8"));
|
|
437
|
+
return isPendingRootSwap(raw) ? raw : null;
|
|
438
|
+
} catch {
|
|
439
|
+
return null;
|
|
394
440
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
441
|
+
}
|
|
442
|
+
__name(readPendingRootSwap, "readPendingRootSwap");
|
|
443
|
+
function writePendingRootSwap(rootDir, marker) {
|
|
444
|
+
fs3.mkdirSync(rootDir, {
|
|
445
|
+
recursive: true
|
|
446
|
+
});
|
|
447
|
+
const target = pendingRootSwapPath(rootDir);
|
|
448
|
+
const tmp = `${target}.tmp`;
|
|
449
|
+
fs3.writeFileSync(tmp, JSON.stringify(marker, null, 2), "utf-8");
|
|
450
|
+
fs3.renameSync(tmp, target);
|
|
451
|
+
}
|
|
452
|
+
__name(writePendingRootSwap, "writePendingRootSwap");
|
|
453
|
+
function clearPendingRootSwap(rootDir) {
|
|
454
|
+
try {
|
|
455
|
+
fs3.rmSync(pendingRootSwapPath(rootDir), {
|
|
456
|
+
force: true
|
|
457
|
+
});
|
|
458
|
+
} catch {
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
__name(clearPendingRootSwap, "clearPendingRootSwap");
|
|
462
|
+
var REQUIRED_NATIVE_PACKAGES = [
|
|
463
|
+
"better-sqlite3",
|
|
464
|
+
"sharp",
|
|
465
|
+
"node-av"
|
|
466
|
+
];
|
|
467
|
+
var NATIVE_SEARCH_DIRS = {
|
|
468
|
+
"better-sqlite3": [
|
|
469
|
+
"better-sqlite3"
|
|
470
|
+
],
|
|
471
|
+
sharp: [
|
|
472
|
+
"sharp",
|
|
473
|
+
"@img"
|
|
474
|
+
],
|
|
475
|
+
"node-av": [
|
|
476
|
+
"node-av",
|
|
477
|
+
"@seydx"
|
|
478
|
+
]
|
|
479
|
+
};
|
|
480
|
+
function hasDotNode(dir, maxDepth) {
|
|
481
|
+
let entries;
|
|
482
|
+
try {
|
|
483
|
+
entries = fs3.readdirSync(dir, {
|
|
484
|
+
withFileTypes: true
|
|
485
|
+
});
|
|
486
|
+
} catch {
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
for (const entry of entries) {
|
|
490
|
+
if (entry.isFile() && entry.name.endsWith(".node")) return true;
|
|
491
|
+
}
|
|
492
|
+
if (maxDepth <= 0) return false;
|
|
493
|
+
for (const entry of entries) {
|
|
494
|
+
if (entry.isDirectory() && hasDotNode(path3.join(dir, entry.name), maxDepth - 1)) return true;
|
|
495
|
+
}
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
__name(hasDotNode, "hasDotNode");
|
|
499
|
+
function findMissingNativePrebuilds(closureDir) {
|
|
500
|
+
const nm = path3.join(closureDir, "node_modules");
|
|
501
|
+
const missing = [];
|
|
502
|
+
for (const pkg of REQUIRED_NATIVE_PACKAGES) {
|
|
503
|
+
const searchDirs = NATIVE_SEARCH_DIRS[pkg] ?? [
|
|
504
|
+
pkg
|
|
505
|
+
];
|
|
506
|
+
const found = searchDirs.some((rel) => hasDotNode(path3.join(nm, ...rel.split("/")), 4));
|
|
507
|
+
if (!found) missing.push(pkg);
|
|
508
|
+
}
|
|
509
|
+
return missing;
|
|
510
|
+
}
|
|
511
|
+
__name(findMissingNativePrebuilds, "findMissingNativePrebuilds");
|
|
512
|
+
function rmrf(target) {
|
|
513
|
+
try {
|
|
514
|
+
fs3.rmSync(target, {
|
|
515
|
+
recursive: true,
|
|
516
|
+
force: true
|
|
517
|
+
});
|
|
518
|
+
} catch {
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
__name(rmrf, "rmrf");
|
|
522
|
+
function errMsg(err) {
|
|
523
|
+
return err instanceof Error ? err.message : String(err);
|
|
524
|
+
}
|
|
525
|
+
__name(errMsg, "errMsg");
|
|
526
|
+
function applyPendingRootSwap(rootDir, spec, nodeMajor, now, log) {
|
|
527
|
+
const marker = readPendingRootSwap(rootDir);
|
|
528
|
+
if (marker === null) return {
|
|
529
|
+
applied: false,
|
|
530
|
+
version: null,
|
|
531
|
+
reason: "no pending swap"
|
|
532
|
+
};
|
|
533
|
+
const staging = marker.stagingPath;
|
|
534
|
+
const dest = currentDir(rootDir);
|
|
535
|
+
const invalid = validateClosureDir(staging, nodeMajor, spec, marker.version);
|
|
536
|
+
if (invalid !== null) {
|
|
537
|
+
if (!fs3.existsSync(staging) && validateClosureDir(dest, nodeMajor, spec, marker.version) === null) {
|
|
538
|
+
clearPendingRootSwap(rootDir);
|
|
539
|
+
log(`[single-copy] pending swap ${marker.version} already applied \u2014 cleared marker`);
|
|
424
540
|
return {
|
|
425
|
-
|
|
426
|
-
version:
|
|
427
|
-
|
|
428
|
-
stateToWrite: {
|
|
429
|
-
...next,
|
|
430
|
-
pendingBoot: {
|
|
431
|
-
...pending,
|
|
432
|
-
bootAttempts: pending.bootAttempts + 1
|
|
433
|
-
}
|
|
434
|
-
}
|
|
541
|
+
applied: true,
|
|
542
|
+
version: marker.version,
|
|
543
|
+
reason: "already applied"
|
|
435
544
|
};
|
|
436
545
|
}
|
|
546
|
+
log(`[single-copy] staged swap ${marker.version} invalid \u2014 discarding (${invalid})`);
|
|
547
|
+
rmrf(staging);
|
|
548
|
+
clearPendingRootSwap(rootDir);
|
|
549
|
+
return {
|
|
550
|
+
applied: false,
|
|
551
|
+
version: null,
|
|
552
|
+
reason: invalid
|
|
553
|
+
};
|
|
437
554
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
555
|
+
const missing = findMissingNativePrebuilds(staging);
|
|
556
|
+
if (missing.length > 0) {
|
|
557
|
+
log(`[single-copy] staged swap ${marker.version} missing native prebuilds: ${missing.join(", ")} \u2014 discarding`);
|
|
558
|
+
rmrf(staging);
|
|
559
|
+
clearPendingRootSwap(rootDir);
|
|
560
|
+
return {
|
|
561
|
+
applied: false,
|
|
562
|
+
version: null,
|
|
563
|
+
reason: `missing native prebuilds: ${missing.join(", ")}`
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
let trash = null;
|
|
567
|
+
try {
|
|
568
|
+
if (fs3.existsSync(dest)) {
|
|
569
|
+
trash = path3.join(rootDir, `.trash-${now()}-${process.pid}`);
|
|
570
|
+
fs3.renameSync(dest, trash);
|
|
571
|
+
}
|
|
572
|
+
try {
|
|
573
|
+
fs3.renameSync(staging, dest);
|
|
574
|
+
} catch (err) {
|
|
575
|
+
if (trash !== null && !fs3.existsSync(dest)) {
|
|
576
|
+
try {
|
|
577
|
+
fs3.renameSync(trash, dest);
|
|
578
|
+
} catch {
|
|
579
|
+
}
|
|
452
580
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
581
|
+
throw err;
|
|
582
|
+
}
|
|
583
|
+
} catch (err) {
|
|
584
|
+
log(`[single-copy] swap of ${marker.version} FAILED \u2014 current preserved (${errMsg(err)})`);
|
|
585
|
+
return {
|
|
586
|
+
applied: false,
|
|
587
|
+
version: null,
|
|
588
|
+
reason: errMsg(err)
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
if (trash !== null) rmrf(trash);
|
|
592
|
+
clearPendingRootSwap(rootDir);
|
|
593
|
+
log(`[single-copy] applied staged root swap \u2192 ${marker.version}`);
|
|
594
|
+
return {
|
|
595
|
+
applied: true,
|
|
596
|
+
version: marker.version,
|
|
597
|
+
reason: "applied"
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
__name(applyPendingRootSwap, "applyPendingRootSwap");
|
|
601
|
+
function readClosureVersion(closureDir, spec) {
|
|
602
|
+
try {
|
|
603
|
+
const raw = JSON.parse(fs3.readFileSync(path3.join(rootPackageDir2(closureDir, spec), "package.json"), "utf-8"));
|
|
604
|
+
if (typeof raw === "object" && raw !== null) {
|
|
605
|
+
const version = raw["version"];
|
|
606
|
+
if (typeof version === "string") return version;
|
|
459
607
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
608
|
+
} catch {
|
|
609
|
+
}
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
__name(readClosureVersion, "readClosureVersion");
|
|
613
|
+
function isAdoptablePendingClosure(rootDir, version, nodeMajor, spec) {
|
|
614
|
+
const dir = versionDir(rootDir, version);
|
|
615
|
+
return validateClosureDir(dir, nodeMajor, spec, version) === null && findMissingNativePrebuilds(dir).length === 0;
|
|
616
|
+
}
|
|
617
|
+
__name(isAdoptablePendingClosure, "isAdoptablePendingClosure");
|
|
618
|
+
function migrateToSingleCopy(rootDir, spec, nodeMajor, now, log) {
|
|
619
|
+
const dest = currentDir(rootDir);
|
|
620
|
+
const legacyState = readServerRootState(rootDir);
|
|
621
|
+
const pendingVersion = legacyState?.pendingBoot?.version ?? null;
|
|
622
|
+
const pendingAdoptable = pendingVersion !== null && isAdoptablePendingClosure(rootDir, pendingVersion, nodeMajor, spec);
|
|
623
|
+
const currentValid = fs3.existsSync(dest) && validateClosureDir(dest, nodeMajor, spec) === null;
|
|
624
|
+
if (currentValid) {
|
|
625
|
+
const currentVersion = readClosureVersion(dest, spec);
|
|
626
|
+
const pendingSupersedes = pendingAdoptable && pendingVersion !== null && (currentVersion === null || compareSemver(pendingVersion, currentVersion) > 0);
|
|
627
|
+
if (!pendingSupersedes) {
|
|
463
628
|
return {
|
|
464
|
-
|
|
465
|
-
version:
|
|
466
|
-
|
|
467
|
-
stateToWrite: {
|
|
468
|
-
...next,
|
|
469
|
-
currentVersion: fallback,
|
|
470
|
-
previousVersion: null,
|
|
471
|
-
rolledBack: {
|
|
472
|
-
fromVersion: broken,
|
|
473
|
-
toVersion: fallback,
|
|
474
|
-
atMs: now(),
|
|
475
|
-
reason: "active version failed validation"
|
|
476
|
-
}
|
|
477
|
-
}
|
|
629
|
+
migrated: false,
|
|
630
|
+
version: null,
|
|
631
|
+
reason: "current already present"
|
|
478
632
|
};
|
|
479
633
|
}
|
|
634
|
+
log(`[single-copy] in-flight pending ${pendingVersion} supersedes older current ${currentVersion ?? "?"}`);
|
|
635
|
+
}
|
|
636
|
+
const candidates = [];
|
|
637
|
+
if (pendingAdoptable && pendingVersion !== null) candidates.push(pendingVersion);
|
|
638
|
+
if (legacyState !== null) {
|
|
639
|
+
for (const v of [
|
|
640
|
+
legacyState.currentVersion,
|
|
641
|
+
legacyState.pendingBoot?.version ?? null,
|
|
642
|
+
legacyState.previousVersion
|
|
643
|
+
]) {
|
|
644
|
+
if (typeof v === "string" && !candidates.includes(v)) candidates.push(v);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
try {
|
|
648
|
+
for (const entry of fs3.readdirSync(versionsDir(rootDir))) {
|
|
649
|
+
if (!entry.startsWith(".") && !candidates.includes(entry)) candidates.push(entry);
|
|
650
|
+
}
|
|
651
|
+
} catch {
|
|
652
|
+
}
|
|
653
|
+
const source = candidates.find((v) => validateClosureDir(versionDir(rootDir, v), nodeMajor, spec, v) === null);
|
|
654
|
+
if (source === void 0) {
|
|
655
|
+
return {
|
|
656
|
+
migrated: false,
|
|
657
|
+
version: null,
|
|
658
|
+
reason: "no valid legacy versions/<X> to adopt"
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
if (fs3.existsSync(dest)) {
|
|
662
|
+
const aside = path3.join(rootDir, `.trash-${now()}-${process.pid}-stale-current`);
|
|
663
|
+
try {
|
|
664
|
+
fs3.renameSync(dest, aside);
|
|
665
|
+
} catch {
|
|
666
|
+
rmrf(dest);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
try {
|
|
670
|
+
fs3.renameSync(versionDir(rootDir, source), dest);
|
|
671
|
+
} catch (err) {
|
|
672
|
+
log(`[single-copy] migration rename of versions/${source} failed (${errMsg(err)})`);
|
|
480
673
|
return {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
674
|
+
migrated: false,
|
|
675
|
+
version: null,
|
|
676
|
+
reason: errMsg(err)
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
rmrf(versionsDir(rootDir));
|
|
680
|
+
rmrf(stateFilePath(rootDir));
|
|
681
|
+
const dataDir = path3.dirname(rootDir);
|
|
682
|
+
for (const marker of LEGACY_FRAMEWORK_MARKERS) rmrf(path3.join(dataDir, marker));
|
|
683
|
+
log(`[single-copy] migrated legacy versions/${source} \u2192 current`);
|
|
684
|
+
return {
|
|
685
|
+
migrated: true,
|
|
686
|
+
version: source,
|
|
687
|
+
reason: "adopted legacy version"
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
__name(migrateToSingleCopy, "migrateToSingleCopy");
|
|
691
|
+
function sweepTransientRootDirs(rootDir, now, staleMs) {
|
|
692
|
+
let entries;
|
|
693
|
+
try {
|
|
694
|
+
entries = fs3.readdirSync(rootDir);
|
|
695
|
+
} catch {
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
for (const entry of entries) {
|
|
699
|
+
if (!entry.startsWith(".staging-") && !entry.startsWith(".trash-")) continue;
|
|
700
|
+
const full = path3.join(rootDir, entry);
|
|
701
|
+
try {
|
|
702
|
+
if (now - fs3.statSync(full).mtimeMs < staleMs) continue;
|
|
703
|
+
} catch {
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
rmrf(full);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
__name(sweepTransientRootDirs, "sweepTransientRootDirs");
|
|
710
|
+
function planBoot(currentValid) {
|
|
711
|
+
if (currentValid) {
|
|
712
|
+
return {
|
|
713
|
+
kind: "current",
|
|
714
|
+
reason: "single-copy closure present and valid"
|
|
493
715
|
};
|
|
494
716
|
}
|
|
495
717
|
return {
|
|
496
718
|
kind: "baked",
|
|
497
|
-
reason: "no
|
|
498
|
-
stateToWrite: changed ? next : null
|
|
719
|
+
reason: "no valid single-copy closure \u2014 booting the baked seed"
|
|
499
720
|
};
|
|
500
721
|
}
|
|
501
722
|
__name(planBoot, "planBoot");
|
|
502
|
-
var
|
|
503
|
-
var
|
|
723
|
+
var fs4 = __toESM2(require("fs"));
|
|
724
|
+
var path4 = __toESM2(require("path"));
|
|
504
725
|
function detectWorkspaceRoot(fromDir) {
|
|
505
|
-
let dir =
|
|
726
|
+
let dir = path4.resolve(fromDir);
|
|
506
727
|
for (; ; ) {
|
|
507
|
-
const pkgPath =
|
|
728
|
+
const pkgPath = path4.join(dir, "package.json");
|
|
508
729
|
try {
|
|
509
|
-
const raw = JSON.parse(
|
|
730
|
+
const raw = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
|
|
510
731
|
if (typeof raw === "object" && raw !== null && raw["workspaces"] !== void 0) {
|
|
511
732
|
return dir;
|
|
512
733
|
}
|
|
513
734
|
} catch {
|
|
514
735
|
}
|
|
515
|
-
const parent =
|
|
736
|
+
const parent = path4.dirname(dir);
|
|
516
737
|
if (parent === dir) return null;
|
|
517
738
|
dir = parent;
|
|
518
739
|
}
|
|
519
740
|
}
|
|
520
741
|
__name(detectWorkspaceRoot, "detectWorkspaceRoot");
|
|
521
|
-
var
|
|
522
|
-
var
|
|
742
|
+
var fs5 = __toESM2(require("fs"));
|
|
743
|
+
var path5 = __toESM2(require("path"));
|
|
523
744
|
var import_node_url = require("url");
|
|
524
745
|
var HOST_EXTERNAL_SPECIFIERS = [
|
|
525
746
|
"@camstack/system",
|
|
@@ -543,7 +764,7 @@ var require_dist = __commonJS({
|
|
|
543
764
|
console.warn("[starter] module.registerHooks unavailable (Node < 22.15) \u2014 host-external redirect skipped");
|
|
544
765
|
return;
|
|
545
766
|
}
|
|
546
|
-
const anchorURL = (0, import_node_url.pathToFileURL)(
|
|
767
|
+
const anchorURL = (0, import_node_url.pathToFileURL)(path5.join(activeRootDir, "node_modules", "__camstack_starter_anchor__.js")).href;
|
|
547
768
|
const hooks = {
|
|
548
769
|
resolve: /* @__PURE__ */ __name((specifier, context, nextResolve) => {
|
|
549
770
|
if (isHostExternal(specifier)) {
|
|
@@ -561,24 +782,23 @@ var require_dist = __commonJS({
|
|
|
561
782
|
registerHooks(hooks);
|
|
562
783
|
}
|
|
563
784
|
__name(registerActiveRootResolver, "registerActiveRootResolver");
|
|
564
|
-
function
|
|
785
|
+
function readClosureVersion2(closureDir, spec) {
|
|
565
786
|
try {
|
|
566
|
-
const
|
|
567
|
-
const raw = JSON.parse(fs4.readFileSync(pkgJsonPath, "utf-8"));
|
|
787
|
+
const raw = JSON.parse(fs5.readFileSync(path5.join(rootPackageDir2(closureDir, spec), "package.json"), "utf-8"));
|
|
568
788
|
if (typeof raw === "object" && raw !== null) {
|
|
569
789
|
const version = raw["version"];
|
|
570
790
|
if (typeof version === "string") return version;
|
|
571
791
|
}
|
|
572
|
-
return null;
|
|
573
792
|
} catch {
|
|
574
|
-
return null;
|
|
575
793
|
}
|
|
794
|
+
return null;
|
|
576
795
|
}
|
|
577
|
-
__name(
|
|
796
|
+
__name(readClosureVersion2, "readClosureVersion2");
|
|
578
797
|
function runNodeStarter(options) {
|
|
579
798
|
const env = options.env ?? process.env;
|
|
580
799
|
const now = options.now ?? Date.now;
|
|
581
800
|
const registerResolver = options.registerResolver ?? registerActiveRootResolver;
|
|
801
|
+
const nodeMajor = options.nodeMajor ?? Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
|
|
582
802
|
const disabled = env[options.envNames.killSwitch] === "off";
|
|
583
803
|
const workspaceRoot = detectWorkspaceRoot(options.starterDir);
|
|
584
804
|
if (workspaceRoot !== null) {
|
|
@@ -594,36 +814,24 @@ var require_dist = __commonJS({
|
|
|
594
814
|
return;
|
|
595
815
|
}
|
|
596
816
|
const rootDir = serverRootDir(options.dataDir);
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
if (plan.stateToWrite !== null) {
|
|
607
|
-
try {
|
|
608
|
-
writeServerRootState(rootDir, plan.stateToWrite);
|
|
609
|
-
} catch (err) {
|
|
610
|
-
console.error("[starter] FAILED to persist server-root state \u2014 booting baked seed:", err);
|
|
611
|
-
env[options.envNames.bootMode] = "baked";
|
|
612
|
-
options.loadEntry(options.seedEntry);
|
|
613
|
-
return;
|
|
614
|
-
}
|
|
615
|
-
if (plan.stateToWrite.rolledBack !== null && (state?.rolledBack ?? null) !== plan.stateToWrite.rolledBack) {
|
|
616
|
-
const rb = plan.stateToWrite.rolledBack;
|
|
617
|
-
console.warn(`[starter] ROLLED BACK ${rb.fromVersion} -> ${rb.toVersion ?? "baked seed"}: ${rb.reason}`);
|
|
618
|
-
}
|
|
817
|
+
const swap = applyPendingRootSwap(rootDir, options.spec, nodeMajor, now, (m) => console.log(m));
|
|
818
|
+
if (swap.applied) console.log(`[starter] applied staged update \u2192 ${swap.version ?? "?"}`);
|
|
819
|
+
else if (swap.reason !== "no pending swap") console.warn(`[starter] staged update NOT applied: ${swap.reason}`);
|
|
820
|
+
const migration = migrateToSingleCopy(rootDir, options.spec, nodeMajor, now, (m) => console.log(m));
|
|
821
|
+
if (migration.migrated) console.log(`[starter] migrated legacy layout \u2192 single copy (${migration.version ?? "?"})`);
|
|
822
|
+
const activeRootDir = currentDir(rootDir);
|
|
823
|
+
const invalid = validateClosureDir(activeRootDir, nodeMajor, options.spec);
|
|
824
|
+
if (invalid !== null && fs5.existsSync(activeRootDir)) {
|
|
825
|
+
console.warn(`[starter] single copy invalid: ${invalid}`);
|
|
619
826
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
const
|
|
623
|
-
|
|
827
|
+
const plan = planBoot(invalid === null);
|
|
828
|
+
if (plan.kind === "current") {
|
|
829
|
+
const version = readClosureVersion2(activeRootDir, options.spec);
|
|
830
|
+
const entry = currentEntryPath(rootDir, options.spec);
|
|
831
|
+
console.log(`[starter] booting ${options.spec.packageName}@${version ?? "?"} from ${activeRootDir}`);
|
|
624
832
|
env[options.envNames.bootMode] = "data-root";
|
|
625
833
|
env[options.envNames.activeRoot] = activeRootDir;
|
|
626
|
-
env[options.envNames.activeVersion] =
|
|
834
|
+
if (version !== null) env[options.envNames.activeVersion] = version;
|
|
627
835
|
registerResolver(activeRootDir);
|
|
628
836
|
options.loadEntry(entry);
|
|
629
837
|
return;
|
|
@@ -634,8 +842,8 @@ var require_dist = __commonJS({
|
|
|
634
842
|
}
|
|
635
843
|
__name(runNodeStarter, "runNodeStarter");
|
|
636
844
|
var import_node_child_process = require("child_process");
|
|
637
|
-
var
|
|
638
|
-
var
|
|
845
|
+
var fs6 = __toESM2(require("fs"));
|
|
846
|
+
var path6 = __toESM2(require("path"));
|
|
639
847
|
var import_node_util = require("util");
|
|
640
848
|
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
641
849
|
function buildNpmRegistryArgs(registry) {
|
|
@@ -650,9 +858,10 @@ var require_dist = __commonJS({
|
|
|
650
858
|
var NPM_VIEW_TIMEOUT_MS = 2e4;
|
|
651
859
|
var NPM_INSTALL_TIMEOUT_MS = 15 * 6e4;
|
|
652
860
|
var RESTART_REASON_PREFIX = "server-update";
|
|
861
|
+
var STALE_TRANSIENT_MS = 24 * 60 * 60 * 1e3;
|
|
653
862
|
function readPackageVersion(pkgJsonPath) {
|
|
654
863
|
try {
|
|
655
|
-
const raw = JSON.parse(
|
|
864
|
+
const raw = JSON.parse(fs6.readFileSync(pkgJsonPath, "utf-8"));
|
|
656
865
|
if (typeof raw === "object" && raw !== null) {
|
|
657
866
|
const version = raw["version"];
|
|
658
867
|
if (typeof version === "string") return version;
|
|
@@ -662,9 +871,9 @@ var require_dist = __commonJS({
|
|
|
662
871
|
return null;
|
|
663
872
|
}
|
|
664
873
|
__name(readPackageVersion, "readPackageVersion");
|
|
665
|
-
var RootUpdateService = class
|
|
874
|
+
var RootUpdateService = class {
|
|
666
875
|
static {
|
|
667
|
-
__name(this, "
|
|
876
|
+
__name(this, "RootUpdateService");
|
|
668
877
|
}
|
|
669
878
|
spec;
|
|
670
879
|
envNames;
|
|
@@ -683,7 +892,7 @@ var require_dist = __commonJS({
|
|
|
683
892
|
this.envNames = options.envNames;
|
|
684
893
|
this.logger = options.logger;
|
|
685
894
|
this.restartServerFn = options.restartServer;
|
|
686
|
-
this.dataDir =
|
|
895
|
+
this.dataDir = path6.resolve(options.dataDir);
|
|
687
896
|
this.execNpm = options.execNpm ?? (async (args, opts) => {
|
|
688
897
|
const { stdout } = await execFileAsync("npm", [
|
|
689
898
|
...args
|
|
@@ -719,63 +928,37 @@ var require_dist = __commonJS({
|
|
|
719
928
|
seedVersion() {
|
|
720
929
|
const seedDir = this.env[this.envNames.seedDir];
|
|
721
930
|
if (seedDir === void 0 || seedDir.length === 0) return null;
|
|
722
|
-
return readPackageVersion(
|
|
931
|
+
return readPackageVersion(path6.join(seedDir, "package.json"));
|
|
723
932
|
}
|
|
724
933
|
rootDir() {
|
|
725
934
|
return serverRootDir(this.dataDir);
|
|
726
935
|
}
|
|
727
|
-
|
|
728
|
-
return readServerRootState(this.rootDir()) ?? emptyServerRootState();
|
|
729
|
-
}
|
|
730
|
-
/**
|
|
731
|
-
* Like `readState`, but distinguishes "state.json missing" (normal on a
|
|
732
|
-
* fresh node) from "state.json present but corrupt" — in the corrupt case
|
|
733
|
-
* the starter booted the baked seed while installed data-dir versions are
|
|
734
|
-
* silently ignored, which the status surface must make visible.
|
|
735
|
-
*/
|
|
736
|
-
readStateInfo() {
|
|
737
|
-
const rootDir = this.rootDir();
|
|
738
|
-
const raw = readServerRootState(rootDir);
|
|
739
|
-
if (raw !== null) return {
|
|
740
|
-
state: raw,
|
|
741
|
-
corrupt: false
|
|
742
|
-
};
|
|
743
|
-
return {
|
|
744
|
-
state: emptyServerRootState(),
|
|
745
|
-
corrupt: fs5.existsSync(stateFilePath(rootDir))
|
|
746
|
-
};
|
|
747
|
-
}
|
|
748
|
-
updateState(state) {
|
|
936
|
+
updateState() {
|
|
749
937
|
if (this.inFlight === "checking") return "checking";
|
|
750
938
|
if (this.inFlight === "staging") return "staging";
|
|
751
|
-
if (
|
|
752
|
-
const activeVersion = this.env[this.envNames.activeVersion] ?? null;
|
|
753
|
-
if (activeVersion !== null && activeVersion === state.pendingBoot.version) {
|
|
754
|
-
return "awaiting-confirmation";
|
|
755
|
-
}
|
|
756
|
-
return "pending-restart";
|
|
757
|
-
}
|
|
939
|
+
if (readPendingRootSwap(this.rootDir()) !== null) return "pending-restart";
|
|
758
940
|
return "idle";
|
|
759
941
|
}
|
|
760
942
|
async getServerPackageStatus() {
|
|
761
|
-
const { state, corrupt } = this.readStateInfo();
|
|
762
943
|
const runningVersion = this.runningVersion();
|
|
763
944
|
const latestVersion = this.checkCache?.latestVersion ?? null;
|
|
945
|
+
const pending = readPendingRootSwap(this.rootDir());
|
|
764
946
|
const updateAvailable = latestVersion !== null && runningVersion !== null && compareSemver(latestVersion, runningVersion) > 0;
|
|
765
947
|
return {
|
|
766
948
|
packageName: this.spec.packageName,
|
|
767
949
|
runningVersion,
|
|
768
950
|
nodeRuntimeVersion: process.versions.node,
|
|
769
|
-
activeVersion: this.env[this.envNames.activeVersion] ??
|
|
770
|
-
|
|
951
|
+
activeVersion: this.env[this.envNames.activeVersion] ?? runningVersion,
|
|
952
|
+
// Single-copy model: no N-1 retention, no rollback record.
|
|
953
|
+
previousVersion: null,
|
|
771
954
|
seedVersion: this.seedVersion(),
|
|
772
955
|
latestVersion,
|
|
773
956
|
updateAvailable,
|
|
774
957
|
bootMode: this.resolveBootMode(),
|
|
775
|
-
updateState: this.updateState(
|
|
776
|
-
pendingVersion:
|
|
777
|
-
rolledBack:
|
|
778
|
-
stateFileCorrupt:
|
|
958
|
+
updateState: this.updateState(),
|
|
959
|
+
pendingVersion: pending?.version ?? null,
|
|
960
|
+
rolledBack: null,
|
|
961
|
+
stateFileCorrupt: false,
|
|
779
962
|
lastCheckedAtMs: this.checkCache?.checkedAtMs ?? null
|
|
780
963
|
};
|
|
781
964
|
}
|
|
@@ -862,13 +1045,13 @@ var require_dist = __commonJS({
|
|
|
862
1045
|
message: `Refused: another operation is in flight (${this.inFlight}).`
|
|
863
1046
|
};
|
|
864
1047
|
}
|
|
865
|
-
const
|
|
866
|
-
if (
|
|
1048
|
+
const pendingBefore = readPendingRootSwap(this.rootDir());
|
|
1049
|
+
if (pendingBefore !== null) {
|
|
867
1050
|
return {
|
|
868
1051
|
accepted: false,
|
|
869
1052
|
targetVersion: input.version ?? null,
|
|
870
1053
|
restarting: false,
|
|
871
|
-
message: `Refused: version ${
|
|
1054
|
+
message: `Refused: version ${pendingBefore.version} is already staged and awaiting restart.`
|
|
872
1055
|
};
|
|
873
1056
|
}
|
|
874
1057
|
let target = input.version ?? null;
|
|
@@ -895,7 +1078,7 @@ var require_dist = __commonJS({
|
|
|
895
1078
|
}
|
|
896
1079
|
if (isDevChannelVersion(target)) {
|
|
897
1080
|
const uploadsDir = devUploadVersionDir(this.rootDir(), target);
|
|
898
|
-
if (!
|
|
1081
|
+
if (!fs6.existsSync(uploadsDir)) {
|
|
899
1082
|
return {
|
|
900
1083
|
accepted: false,
|
|
901
1084
|
targetVersion: target,
|
|
@@ -905,8 +1088,9 @@ var require_dist = __commonJS({
|
|
|
905
1088
|
}
|
|
906
1089
|
}
|
|
907
1090
|
this.inFlight = "staging";
|
|
1091
|
+
let stagingPath;
|
|
908
1092
|
try {
|
|
909
|
-
await this.
|
|
1093
|
+
stagingPath = await this.stageClosure(target);
|
|
910
1094
|
} catch (err) {
|
|
911
1095
|
const message = err instanceof Error ? err.message : String(err);
|
|
912
1096
|
this.logger.error("root package update staging failed", {
|
|
@@ -924,21 +1108,16 @@ var require_dist = __commonJS({
|
|
|
924
1108
|
} finally {
|
|
925
1109
|
this.inFlight = "idle";
|
|
926
1110
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
fromVersion: state.currentVersion,
|
|
933
|
-
requestedAtMs: this.now(),
|
|
934
|
-
bootAttempts: 0
|
|
935
|
-
},
|
|
936
|
-
rolledBack: null
|
|
1111
|
+
writePendingRootSwap(this.rootDir(), {
|
|
1112
|
+
schemaVersion: 1,
|
|
1113
|
+
version: target,
|
|
1114
|
+
stagingPath,
|
|
1115
|
+
requestedAtMs: this.now()
|
|
937
1116
|
});
|
|
938
1117
|
this.logger.info("root package update staged \u2014 restarting to apply", {
|
|
939
1118
|
meta: {
|
|
940
1119
|
targetVersion: target,
|
|
941
|
-
fromVersion:
|
|
1120
|
+
fromVersion: runningVersion
|
|
942
1121
|
}
|
|
943
1122
|
});
|
|
944
1123
|
this.restartServerFn(`${RESTART_REASON_PREFIX}: ${this.spec.packageName}@${target}`);
|
|
@@ -946,21 +1125,23 @@ var require_dist = __commonJS({
|
|
|
946
1125
|
accepted: true,
|
|
947
1126
|
targetVersion: target,
|
|
948
1127
|
restarting: true,
|
|
949
|
-
message: `Staged ${this.spec.packageName}@${target} \u2014 restarting to apply (
|
|
1128
|
+
message: `Staged ${this.spec.packageName}@${target} \u2014 restarting to apply (single automatic reboot).`
|
|
950
1129
|
};
|
|
951
1130
|
}
|
|
952
1131
|
/**
|
|
953
|
-
* npm-install the target closure into a SAME-FS staging dir
|
|
954
|
-
*
|
|
1132
|
+
* npm-install the target closure into a SAME-FS `.staging-*` dir, then
|
|
1133
|
+
* validate it (entry + version + native prebuilds). Returns the staging dir
|
|
1134
|
+
* path for the pending-root-swap marker; the STARTER swaps it into `current`
|
|
1135
|
+
* on the next boot. Throws (cleaning up the staging dir) on any failure so
|
|
1136
|
+
* `applyServerUpdate` reports a staging error and never arms a bad swap.
|
|
955
1137
|
*/
|
|
956
|
-
async
|
|
1138
|
+
async stageClosure(target) {
|
|
957
1139
|
const rootDir = this.rootDir();
|
|
958
|
-
|
|
959
|
-
fs5.mkdirSync(vDir, {
|
|
1140
|
+
fs6.mkdirSync(rootDir, {
|
|
960
1141
|
recursive: true
|
|
961
1142
|
});
|
|
962
|
-
const stagingDir =
|
|
963
|
-
|
|
1143
|
+
const stagingDir = stagingDirPath(rootDir, target, process.pid, this.now());
|
|
1144
|
+
fs6.mkdirSync(stagingDir, {
|
|
964
1145
|
recursive: true
|
|
965
1146
|
});
|
|
966
1147
|
try {
|
|
@@ -973,8 +1154,8 @@ var require_dist = __commonJS({
|
|
|
973
1154
|
"--no-fund",
|
|
974
1155
|
"--loglevel=error"
|
|
975
1156
|
];
|
|
976
|
-
if (
|
|
977
|
-
|
|
1157
|
+
if (fs6.existsSync(devDir)) {
|
|
1158
|
+
fs6.writeFileSync(path6.join(stagingDir, "package.json"), JSON.stringify(this.buildDevUploadsPackageJson(devDir, target), null, 2), "utf-8");
|
|
978
1159
|
await this.execNpm([
|
|
979
1160
|
...installArgs,
|
|
980
1161
|
...buildNpmRegistryArgs(registry)
|
|
@@ -983,7 +1164,7 @@ var require_dist = __commonJS({
|
|
|
983
1164
|
timeoutMs: NPM_INSTALL_TIMEOUT_MS
|
|
984
1165
|
});
|
|
985
1166
|
} else {
|
|
986
|
-
|
|
1167
|
+
fs6.writeFileSync(path6.join(stagingDir, "package.json"), JSON.stringify({
|
|
987
1168
|
name: "camstack-node-root",
|
|
988
1169
|
private: true
|
|
989
1170
|
}, null, 2), "utf-8");
|
|
@@ -997,25 +1178,21 @@ var require_dist = __commonJS({
|
|
|
997
1178
|
});
|
|
998
1179
|
}
|
|
999
1180
|
const entry = rootEntryPath2(stagingDir, this.spec);
|
|
1000
|
-
if (!
|
|
1181
|
+
if (!fs6.existsSync(entry)) {
|
|
1001
1182
|
throw new Error(`staged closure is missing the root entry (${entry})`);
|
|
1002
1183
|
}
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1005
|
-
|
|
1184
|
+
const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
|
|
1185
|
+
const invalid = validateClosureDir(stagingDir, nodeMajor, this.spec, target);
|
|
1186
|
+
if (invalid !== null) {
|
|
1187
|
+
throw new Error(`staged closure invalid: ${invalid}`);
|
|
1006
1188
|
}
|
|
1007
|
-
const
|
|
1008
|
-
if (
|
|
1009
|
-
|
|
1010
|
-
await fs5.promises.rename(dest, aside);
|
|
1011
|
-
await fs5.promises.rm(aside, {
|
|
1012
|
-
recursive: true,
|
|
1013
|
-
force: true
|
|
1014
|
-
}).catch(() => void 0);
|
|
1189
|
+
const missingNatives = findMissingNativePrebuilds(stagingDir);
|
|
1190
|
+
if (missingNatives.length > 0) {
|
|
1191
|
+
throw new Error(`staged closure missing native prebuilds: ${missingNatives.join(", ")} \u2014 refusing to arm a swap that would break the forked runners`);
|
|
1015
1192
|
}
|
|
1016
|
-
|
|
1193
|
+
return stagingDir;
|
|
1017
1194
|
} catch (err) {
|
|
1018
|
-
await
|
|
1195
|
+
await fs6.promises.rm(stagingDir, {
|
|
1019
1196
|
recursive: true,
|
|
1020
1197
|
force: true
|
|
1021
1198
|
}).catch(() => void 0);
|
|
@@ -1025,11 +1202,9 @@ var require_dist = __commonJS({
|
|
|
1025
1202
|
/**
|
|
1026
1203
|
* Synthetic staging package.json for the dev server-deploy channel:
|
|
1027
1204
|
* `dependencies` carries the root package as a `file:` ref to its uploaded
|
|
1028
|
-
* tgz; `overrides` maps EVERY OTHER uploaded `@camstack/<name>` to its tgz
|
|
1029
|
-
* so any transitive occurrence resolves to the uploaded copy, never npm.
|
|
1205
|
+
* tgz; `overrides` maps EVERY OTHER uploaded `@camstack/<name>` to its tgz.
|
|
1030
1206
|
* Fail-closed: a missing/invalid manifest, a manifest for a different
|
|
1031
|
-
* version, a missing root package, or a listed-but-absent tgz all throw
|
|
1032
|
-
* (surfaced by `applyServerUpdate` as a staging failure).
|
|
1207
|
+
* version, a missing root package, or a listed-but-absent tgz all throw.
|
|
1033
1208
|
*/
|
|
1034
1209
|
buildDevUploadsPackageJson(devDir, target) {
|
|
1035
1210
|
const manifest = readDevUploadManifest(devDir);
|
|
@@ -1044,8 +1219,8 @@ var require_dist = __commonJS({
|
|
|
1044
1219
|
throw new Error(`dev-uploads for ${target} do not include the root package ${this.spec.packageName}`);
|
|
1045
1220
|
}
|
|
1046
1221
|
const fileRef = /* @__PURE__ */ __name((filename) => {
|
|
1047
|
-
const abs =
|
|
1048
|
-
if (!
|
|
1222
|
+
const abs = path6.join(devDir, filename);
|
|
1223
|
+
if (!fs6.existsSync(abs)) {
|
|
1049
1224
|
throw new Error(`dev-uploads tarball listed in the manifest is missing: ${abs}`);
|
|
1050
1225
|
}
|
|
1051
1226
|
return `file:${abs}`;
|
|
@@ -1066,73 +1241,28 @@ var require_dist = __commonJS({
|
|
|
1066
1241
|
} : {}
|
|
1067
1242
|
};
|
|
1068
1243
|
}
|
|
1069
|
-
// ── Rollback
|
|
1244
|
+
// ── Rollback (removed — single-copy has no N-1) ────────────────────────
|
|
1245
|
+
/**
|
|
1246
|
+
* Rollback is NOT supported in the single-copy model — there is no retained
|
|
1247
|
+
* N-1 copy to revert to (operator's accepted tradeoff). Recovery is manual:
|
|
1248
|
+
* re-apply a known-good version via {@link applyServerUpdate}, or reinstall
|
|
1249
|
+
* the image seed. Kept on the surface so the cap contract is unchanged.
|
|
1250
|
+
*/
|
|
1070
1251
|
async rollbackServerUpdate() {
|
|
1071
|
-
if (this.inFlight !== "idle") {
|
|
1072
|
-
return {
|
|
1073
|
-
accepted: false,
|
|
1074
|
-
targetVersion: null,
|
|
1075
|
-
restarting: false,
|
|
1076
|
-
message: `Refused: another operation is in flight (${this.inFlight}).`
|
|
1077
|
-
};
|
|
1078
|
-
}
|
|
1079
|
-
const state = this.readState();
|
|
1080
|
-
if (state.pendingBoot !== null) {
|
|
1081
|
-
return {
|
|
1082
|
-
accepted: false,
|
|
1083
|
-
targetVersion: null,
|
|
1084
|
-
restarting: false,
|
|
1085
|
-
message: `Refused: version ${state.pendingBoot.version} is already staged and awaiting restart.`
|
|
1086
|
-
};
|
|
1087
|
-
}
|
|
1088
|
-
if (state.currentVersion === null || state.previousVersion === null) {
|
|
1089
|
-
return {
|
|
1090
|
-
accepted: false,
|
|
1091
|
-
targetVersion: state.previousVersion,
|
|
1092
|
-
restarting: false,
|
|
1093
|
-
message: "Refused: no previous version available to roll back to."
|
|
1094
|
-
};
|
|
1095
|
-
}
|
|
1096
|
-
const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
|
|
1097
|
-
const invalidReason = validateVersionDir(this.rootDir(), state.previousVersion, nodeMajor, this.spec);
|
|
1098
|
-
if (invalidReason !== null) {
|
|
1099
|
-
return {
|
|
1100
|
-
accepted: false,
|
|
1101
|
-
targetVersion: state.previousVersion,
|
|
1102
|
-
restarting: false,
|
|
1103
|
-
message: `Refused: previous version ${state.previousVersion} is not loadable (${invalidReason}).`
|
|
1104
|
-
};
|
|
1105
|
-
}
|
|
1106
|
-
writeServerRootState(this.rootDir(), {
|
|
1107
|
-
...state,
|
|
1108
|
-
pendingBoot: {
|
|
1109
|
-
version: state.previousVersion,
|
|
1110
|
-
fromVersion: state.currentVersion,
|
|
1111
|
-
requestedAtMs: this.now(),
|
|
1112
|
-
bootAttempts: 0
|
|
1113
|
-
}
|
|
1114
|
-
});
|
|
1115
|
-
this.logger.info("root package rollback requested \u2014 restarting", {
|
|
1116
|
-
meta: {
|
|
1117
|
-
fromVersion: state.currentVersion,
|
|
1118
|
-
toVersion: state.previousVersion
|
|
1119
|
-
}
|
|
1120
|
-
});
|
|
1121
|
-
this.restartServerFn(`${RESTART_REASON_PREFIX}: rollback to ${this.spec.packageName}@${state.previousVersion}`);
|
|
1122
1252
|
return {
|
|
1123
|
-
accepted:
|
|
1124
|
-
targetVersion:
|
|
1125
|
-
restarting:
|
|
1126
|
-
message:
|
|
1253
|
+
accepted: false,
|
|
1254
|
+
targetVersion: null,
|
|
1255
|
+
restarting: false,
|
|
1256
|
+
message: "Rollback is not supported in the single-copy model (no retained previous version). Recover by re-applying a known-good version via applyServerUpdate, or reinstall the image seed."
|
|
1127
1257
|
};
|
|
1128
1258
|
}
|
|
1129
1259
|
// ── Plain restart ─────────────────────────────────────────────────────
|
|
1130
1260
|
/**
|
|
1131
|
-
* Plain process restart — no version change.
|
|
1132
|
-
*
|
|
1133
|
-
*
|
|
1134
|
-
*
|
|
1135
|
-
*
|
|
1261
|
+
* Plain process restart — no version change. Refuse while a stage is in
|
|
1262
|
+
* flight (a concurrent npm install must not be interrupted) or while a
|
|
1263
|
+
* version is already staged awaiting restart (a naive bounce would boot the
|
|
1264
|
+
* OLD version and orphan the pending swap — the operator should apply
|
|
1265
|
+
* instead).
|
|
1136
1266
|
*/
|
|
1137
1267
|
restartNode() {
|
|
1138
1268
|
const runningVersion = this.runningVersion();
|
|
@@ -1144,13 +1274,12 @@ var require_dist = __commonJS({
|
|
|
1144
1274
|
message: `Refused: another operation is in flight (${this.inFlight}).`
|
|
1145
1275
|
};
|
|
1146
1276
|
}
|
|
1147
|
-
|
|
1148
|
-
if (state.pendingBoot !== null) {
|
|
1277
|
+
if (readPendingRootSwap(this.rootDir()) !== null) {
|
|
1149
1278
|
return {
|
|
1150
1279
|
accepted: false,
|
|
1151
1280
|
targetVersion: runningVersion,
|
|
1152
1281
|
restarting: false,
|
|
1153
|
-
message:
|
|
1282
|
+
message: "Refused: a version is staged and awaiting restart \u2014 apply it instead of a plain restart."
|
|
1154
1283
|
};
|
|
1155
1284
|
}
|
|
1156
1285
|
this.logger.info("plain node restart requested", {
|
|
@@ -1168,118 +1297,37 @@ var require_dist = __commonJS({
|
|
|
1168
1297
|
}
|
|
1169
1298
|
// ── Boot health confirmation ──────────────────────────────────────────
|
|
1170
1299
|
/**
|
|
1171
|
-
* Called by the post-boot path once the node is READY
|
|
1172
|
-
*
|
|
1173
|
-
* the
|
|
1174
|
-
*
|
|
1175
|
-
*
|
|
1176
|
-
* dir beyond {current, previous}. No-op otherwise.
|
|
1300
|
+
* Called by the post-boot path once the node is READY. In the single-copy
|
|
1301
|
+
* model there is no pending version to promote (the swap already happened in
|
|
1302
|
+
* the starter). This becomes a best-effort GC of orphaned transient dirs
|
|
1303
|
+
* (`.staging-*` / `.trash-*` left by a crash mid-swap) + the dev-uploads
|
|
1304
|
+
* sweep. Kept returning the legacy `{ promoted }` shape for the callers.
|
|
1177
1305
|
*/
|
|
1178
1306
|
confirmBootHealthy() {
|
|
1179
|
-
|
|
1180
|
-
if (state === null || state.pendingBoot === null) return {
|
|
1181
|
-
promoted: null
|
|
1182
|
-
};
|
|
1183
|
-
const activeVersion = this.env[this.envNames.activeVersion] ?? null;
|
|
1184
|
-
if (activeVersion !== state.pendingBoot.version) {
|
|
1185
|
-
return {
|
|
1186
|
-
promoted: null
|
|
1187
|
-
};
|
|
1188
|
-
}
|
|
1189
|
-
const promoted = {
|
|
1190
|
-
...state,
|
|
1191
|
-
currentVersion: state.pendingBoot.version,
|
|
1192
|
-
previousVersion: state.pendingBoot.fromVersion,
|
|
1193
|
-
pendingBoot: null,
|
|
1194
|
-
rolledBack: null
|
|
1195
|
-
};
|
|
1196
|
-
writeServerRootState(this.rootDir(), promoted);
|
|
1197
|
-
this.pruneVersions([
|
|
1198
|
-
promoted.currentVersion,
|
|
1199
|
-
promoted.previousVersion
|
|
1200
|
-
].filter((v) => typeof v === "string"));
|
|
1307
|
+
sweepTransientRootDirs(this.rootDir(), this.now(), STALE_TRANSIENT_MS);
|
|
1201
1308
|
this.sweepDevUploads(DEV_UPLOADS_KEEP_COUNT);
|
|
1202
|
-
this.logger.info("root package update confirmed healthy", {
|
|
1203
|
-
meta: {
|
|
1204
|
-
version: promoted.currentVersion,
|
|
1205
|
-
previousVersion: promoted.previousVersion
|
|
1206
|
-
}
|
|
1207
|
-
});
|
|
1208
1309
|
return {
|
|
1209
|
-
promoted:
|
|
1310
|
+
promoted: null
|
|
1210
1311
|
};
|
|
1211
1312
|
}
|
|
1212
1313
|
/**
|
|
1213
|
-
* Remove version dirs under `versions/` not in `keep`.
|
|
1214
|
-
*
|
|
1215
|
-
* Transient dot-entries (`.staging-*` / `.evicted-*`) are NOT pruned unless
|
|
1216
|
-
* older than {@link RootUpdateService.STALE_TRANSIENT_MS}: the API starts
|
|
1217
|
-
* serving BEFORE the post-boot `confirmBootHealthy()` runs, so a concurrent
|
|
1218
|
-
* `applyServerUpdate` can be mid-npm-install into a fresh `.staging-*` dir
|
|
1219
|
-
* when this prune fires — deleting it would corrupt that install. Fresh
|
|
1220
|
-
* transient dirs are owned by their in-flight operation (which cleans up on
|
|
1221
|
-
* failure); the staleness sweep only collects dirs orphaned by a crash.
|
|
1222
|
-
*/
|
|
1223
|
-
pruneVersions(keep) {
|
|
1224
|
-
const vDir = versionsDir(this.rootDir());
|
|
1225
|
-
let entries;
|
|
1226
|
-
try {
|
|
1227
|
-
entries = fs5.readdirSync(vDir);
|
|
1228
|
-
} catch {
|
|
1229
|
-
return;
|
|
1230
|
-
}
|
|
1231
|
-
for (const entry of entries) {
|
|
1232
|
-
if (keep.includes(entry)) continue;
|
|
1233
|
-
const full = path5.join(vDir, entry);
|
|
1234
|
-
if (entry.startsWith(".")) {
|
|
1235
|
-
try {
|
|
1236
|
-
const ageMs = this.now() - fs5.statSync(full).mtimeMs;
|
|
1237
|
-
if (ageMs < _RootUpdateService.STALE_TRANSIENT_MS) continue;
|
|
1238
|
-
} catch {
|
|
1239
|
-
continue;
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
try {
|
|
1243
|
-
fs5.rmSync(full, {
|
|
1244
|
-
recursive: true,
|
|
1245
|
-
force: true
|
|
1246
|
-
});
|
|
1247
|
-
this.logger.debug("pruned server-root version dir", {
|
|
1248
|
-
meta: {
|
|
1249
|
-
entry
|
|
1250
|
-
}
|
|
1251
|
-
});
|
|
1252
|
-
} catch (err) {
|
|
1253
|
-
this.logger.warn("failed to prune server-root version dir", {
|
|
1254
|
-
meta: {
|
|
1255
|
-
entry,
|
|
1256
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1257
|
-
}
|
|
1258
|
-
});
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
/**
|
|
1263
1314
|
* Promote-time sweep of `server-root/dev-uploads/`: keep only the
|
|
1264
1315
|
* `keepCount` most recent entries (ordered by the self-describing
|
|
1265
1316
|
* `-dev.<epochSeconds>` suffix; non-dev-shaped residue counts as oldest).
|
|
1266
|
-
* Uses the same rename-aside graveyard pattern as the addon installer —
|
|
1267
|
-
* a rename is a metadata op that succeeds even when a tarball is held
|
|
1268
|
-
* open, and residue in the graveyard is re-swept on the next promote.
|
|
1269
1317
|
*/
|
|
1270
1318
|
sweepDevUploads(keepCount) {
|
|
1271
1319
|
const dir = devUploadsDir(this.rootDir());
|
|
1272
1320
|
let entries;
|
|
1273
1321
|
try {
|
|
1274
|
-
entries =
|
|
1322
|
+
entries = fs6.readdirSync(dir);
|
|
1275
1323
|
} catch {
|
|
1276
1324
|
return;
|
|
1277
1325
|
}
|
|
1278
|
-
const graveyard =
|
|
1326
|
+
const graveyard = path6.join(dir, ".sweeping");
|
|
1279
1327
|
try {
|
|
1280
|
-
for (const residue of
|
|
1328
|
+
for (const residue of fs6.readdirSync(graveyard)) {
|
|
1281
1329
|
try {
|
|
1282
|
-
|
|
1330
|
+
fs6.rmSync(path6.join(graveyard, residue), {
|
|
1283
1331
|
recursive: true,
|
|
1284
1332
|
force: true
|
|
1285
1333
|
});
|
|
@@ -1294,13 +1342,13 @@ var require_dist = __commonJS({
|
|
|
1294
1342
|
].sort((a, b) => (devChannelEpoch(b) ?? -1) - (devChannelEpoch(a) ?? -1));
|
|
1295
1343
|
const doomed = byNewestFirst.slice(keepCount);
|
|
1296
1344
|
if (doomed.length === 0) return;
|
|
1297
|
-
|
|
1345
|
+
fs6.mkdirSync(graveyard, {
|
|
1298
1346
|
recursive: true
|
|
1299
1347
|
});
|
|
1300
1348
|
for (const entry of doomed) {
|
|
1301
|
-
const aside =
|
|
1349
|
+
const aside = path6.join(graveyard, `${entry}-${this.now()}`);
|
|
1302
1350
|
try {
|
|
1303
|
-
|
|
1351
|
+
fs6.renameSync(path6.join(dir, entry), aside);
|
|
1304
1352
|
} catch (err) {
|
|
1305
1353
|
this.logger.warn("failed to move dev-uploads entry aside for sweep", {
|
|
1306
1354
|
meta: {
|
|
@@ -1311,7 +1359,7 @@ var require_dist = __commonJS({
|
|
|
1311
1359
|
continue;
|
|
1312
1360
|
}
|
|
1313
1361
|
try {
|
|
1314
|
-
|
|
1362
|
+
fs6.rmSync(aside, {
|
|
1315
1363
|
recursive: true,
|
|
1316
1364
|
force: true
|
|
1317
1365
|
});
|
|
@@ -1324,8 +1372,6 @@ var require_dist = __commonJS({
|
|
|
1324
1372
|
}
|
|
1325
1373
|
}
|
|
1326
1374
|
}
|
|
1327
|
-
/** Transient (.staging- / .evicted-) dirs younger than this are never pruned. */
|
|
1328
|
-
static STALE_TRANSIENT_MS = 24 * 60 * 60 * 1e3;
|
|
1329
1375
|
};
|
|
1330
1376
|
}
|
|
1331
1377
|
});
|