@camstack/server 1.1.70 → 1.1.71
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/core/addon/addon-registry.service.js +26 -17
- package/dist/core/server-update/server-update.service.js +10 -10
- package/dist/launcher.js +13 -0
- package/dist/server-root/index.js +436 -416
- package/dist/starter.js +3 -3
- package/package.json +4 -4
- package/dist/core/addon/system-addons-sync.js +0 -121
|
@@ -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,304 @@ 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"));
|
|
374
|
+
var CURRENT_DIRNAME = "current";
|
|
375
|
+
var PENDING_ROOT_SWAP_FILE = ".pending-root-swap.json";
|
|
376
|
+
var LEGACY_FRAMEWORK_MARKERS = [
|
|
377
|
+
".pending-framework-swap.json",
|
|
378
|
+
".framework-swap-confirm.json"
|
|
379
|
+
];
|
|
380
|
+
function currentDir(rootDir) {
|
|
381
|
+
return path3.join(rootDir, CURRENT_DIRNAME);
|
|
382
|
+
}
|
|
383
|
+
__name(currentDir, "currentDir");
|
|
384
|
+
function currentEntryPath(rootDir, spec) {
|
|
385
|
+
return rootEntryPath2(currentDir(rootDir), spec);
|
|
386
|
+
}
|
|
387
|
+
__name(currentEntryPath, "currentEntryPath");
|
|
388
|
+
function stagingDirPath(rootDir, target, pid, now) {
|
|
389
|
+
return path3.join(rootDir, `.staging-${target}-${pid}-${now}`);
|
|
390
|
+
}
|
|
391
|
+
__name(stagingDirPath, "stagingDirPath");
|
|
392
|
+
function pendingRootSwapPath(rootDir) {
|
|
393
|
+
return path3.join(rootDir, PENDING_ROOT_SWAP_FILE);
|
|
394
|
+
}
|
|
395
|
+
__name(pendingRootSwapPath, "pendingRootSwapPath");
|
|
396
|
+
function isPendingRootSwap(v) {
|
|
397
|
+
if (typeof v !== "object" || v === null) return false;
|
|
398
|
+
const m = v;
|
|
399
|
+
return m["schemaVersion"] === 1 && typeof m["version"] === "string" && typeof m["stagingPath"] === "string" && typeof m["requestedAtMs"] === "number";
|
|
400
|
+
}
|
|
401
|
+
__name(isPendingRootSwap, "isPendingRootSwap");
|
|
402
|
+
function readPendingRootSwap(rootDir) {
|
|
403
|
+
try {
|
|
404
|
+
const raw = JSON.parse(fs3.readFileSync(pendingRootSwapPath(rootDir), "utf-8"));
|
|
405
|
+
return isPendingRootSwap(raw) ? raw : null;
|
|
406
|
+
} catch {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
__name(readPendingRootSwap, "readPendingRootSwap");
|
|
411
|
+
function writePendingRootSwap(rootDir, marker) {
|
|
412
|
+
fs3.mkdirSync(rootDir, {
|
|
413
|
+
recursive: true
|
|
414
|
+
});
|
|
415
|
+
const target = pendingRootSwapPath(rootDir);
|
|
416
|
+
const tmp = `${target}.tmp`;
|
|
417
|
+
fs3.writeFileSync(tmp, JSON.stringify(marker, null, 2), "utf-8");
|
|
418
|
+
fs3.renameSync(tmp, target);
|
|
419
|
+
}
|
|
420
|
+
__name(writePendingRootSwap, "writePendingRootSwap");
|
|
421
|
+
function clearPendingRootSwap(rootDir) {
|
|
422
|
+
try {
|
|
423
|
+
fs3.rmSync(pendingRootSwapPath(rootDir), {
|
|
424
|
+
force: true
|
|
425
|
+
});
|
|
426
|
+
} catch {
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
__name(clearPendingRootSwap, "clearPendingRootSwap");
|
|
430
|
+
var REQUIRED_NATIVE_PACKAGES = [
|
|
431
|
+
"better-sqlite3",
|
|
432
|
+
"sharp",
|
|
433
|
+
"node-av"
|
|
434
|
+
];
|
|
435
|
+
var NATIVE_SEARCH_DIRS = {
|
|
436
|
+
"better-sqlite3": [
|
|
437
|
+
"better-sqlite3"
|
|
438
|
+
],
|
|
439
|
+
sharp: [
|
|
440
|
+
"sharp",
|
|
441
|
+
"@img"
|
|
442
|
+
],
|
|
443
|
+
"node-av": [
|
|
444
|
+
"node-av",
|
|
445
|
+
"@seydx"
|
|
446
|
+
]
|
|
447
|
+
};
|
|
448
|
+
function hasDotNode(dir, maxDepth) {
|
|
449
|
+
let entries;
|
|
450
|
+
try {
|
|
451
|
+
entries = fs3.readdirSync(dir, {
|
|
452
|
+
withFileTypes: true
|
|
453
|
+
});
|
|
454
|
+
} catch {
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
for (const entry of entries) {
|
|
458
|
+
if (entry.isFile() && entry.name.endsWith(".node")) return true;
|
|
459
|
+
}
|
|
460
|
+
if (maxDepth <= 0) return false;
|
|
461
|
+
for (const entry of entries) {
|
|
462
|
+
if (entry.isDirectory() && hasDotNode(path3.join(dir, entry.name), maxDepth - 1)) return true;
|
|
463
|
+
}
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
__name(hasDotNode, "hasDotNode");
|
|
467
|
+
function findMissingNativePrebuilds(closureDir) {
|
|
468
|
+
const nm = path3.join(closureDir, "node_modules");
|
|
469
|
+
const missing = [];
|
|
470
|
+
for (const pkg of REQUIRED_NATIVE_PACKAGES) {
|
|
471
|
+
const searchDirs = NATIVE_SEARCH_DIRS[pkg] ?? [
|
|
472
|
+
pkg
|
|
473
|
+
];
|
|
474
|
+
const found = searchDirs.some((rel) => hasDotNode(path3.join(nm, ...rel.split("/")), 4));
|
|
475
|
+
if (!found) missing.push(pkg);
|
|
476
|
+
}
|
|
477
|
+
return missing;
|
|
478
|
+
}
|
|
479
|
+
__name(findMissingNativePrebuilds, "findMissingNativePrebuilds");
|
|
480
|
+
function rmrf(target) {
|
|
481
|
+
try {
|
|
482
|
+
fs3.rmSync(target, {
|
|
483
|
+
recursive: true,
|
|
484
|
+
force: true
|
|
485
|
+
});
|
|
486
|
+
} catch {
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
__name(rmrf, "rmrf");
|
|
490
|
+
function errMsg(err) {
|
|
491
|
+
return err instanceof Error ? err.message : String(err);
|
|
492
|
+
}
|
|
493
|
+
__name(errMsg, "errMsg");
|
|
494
|
+
function applyPendingRootSwap(rootDir, spec, nodeMajor, now, log) {
|
|
495
|
+
const marker = readPendingRootSwap(rootDir);
|
|
496
|
+
if (marker === null) return {
|
|
497
|
+
applied: false,
|
|
498
|
+
version: null,
|
|
499
|
+
reason: "no pending swap"
|
|
500
|
+
};
|
|
501
|
+
const staging = marker.stagingPath;
|
|
502
|
+
const dest = currentDir(rootDir);
|
|
503
|
+
const invalid = validateClosureDir(staging, nodeMajor, spec, marker.version);
|
|
504
|
+
if (invalid !== null) {
|
|
505
|
+
if (!fs3.existsSync(staging) && validateClosureDir(dest, nodeMajor, spec, marker.version) === null) {
|
|
506
|
+
clearPendingRootSwap(rootDir);
|
|
507
|
+
log(`[single-copy] pending swap ${marker.version} already applied \u2014 cleared marker`);
|
|
508
|
+
return {
|
|
509
|
+
applied: true,
|
|
510
|
+
version: marker.version,
|
|
511
|
+
reason: "already applied"
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
log(`[single-copy] staged swap ${marker.version} invalid \u2014 discarding (${invalid})`);
|
|
515
|
+
rmrf(staging);
|
|
516
|
+
clearPendingRootSwap(rootDir);
|
|
517
|
+
return {
|
|
518
|
+
applied: false,
|
|
519
|
+
version: null,
|
|
520
|
+
reason: invalid
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
const missing = findMissingNativePrebuilds(staging);
|
|
524
|
+
if (missing.length > 0) {
|
|
525
|
+
log(`[single-copy] staged swap ${marker.version} missing native prebuilds: ${missing.join(", ")} \u2014 discarding`);
|
|
526
|
+
rmrf(staging);
|
|
527
|
+
clearPendingRootSwap(rootDir);
|
|
528
|
+
return {
|
|
529
|
+
applied: false,
|
|
530
|
+
version: null,
|
|
531
|
+
reason: `missing native prebuilds: ${missing.join(", ")}`
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
let trash = null;
|
|
535
|
+
try {
|
|
536
|
+
if (fs3.existsSync(dest)) {
|
|
537
|
+
trash = path3.join(rootDir, `.trash-${now()}-${process.pid}`);
|
|
538
|
+
fs3.renameSync(dest, trash);
|
|
539
|
+
}
|
|
540
|
+
try {
|
|
541
|
+
fs3.renameSync(staging, dest);
|
|
542
|
+
} catch (err) {
|
|
543
|
+
if (trash !== null && !fs3.existsSync(dest)) {
|
|
544
|
+
try {
|
|
545
|
+
fs3.renameSync(trash, dest);
|
|
546
|
+
} catch {
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
throw err;
|
|
550
|
+
}
|
|
551
|
+
} catch (err) {
|
|
552
|
+
log(`[single-copy] swap of ${marker.version} FAILED \u2014 current preserved (${errMsg(err)})`);
|
|
553
|
+
return {
|
|
554
|
+
applied: false,
|
|
555
|
+
version: null,
|
|
556
|
+
reason: errMsg(err)
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
if (trash !== null) rmrf(trash);
|
|
560
|
+
clearPendingRootSwap(rootDir);
|
|
561
|
+
log(`[single-copy] applied staged root swap \u2192 ${marker.version}`);
|
|
562
|
+
return {
|
|
563
|
+
applied: true,
|
|
564
|
+
version: marker.version,
|
|
565
|
+
reason: "applied"
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
__name(applyPendingRootSwap, "applyPendingRootSwap");
|
|
569
|
+
function migrateToSingleCopy(rootDir, spec, nodeMajor, now, log) {
|
|
570
|
+
const dest = currentDir(rootDir);
|
|
571
|
+
if (fs3.existsSync(dest) && validateClosureDir(dest, nodeMajor, spec) === null) {
|
|
572
|
+
return {
|
|
573
|
+
migrated: false,
|
|
574
|
+
version: null,
|
|
575
|
+
reason: "current already present"
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
const candidates = [];
|
|
579
|
+
const legacyState = readServerRootState(rootDir);
|
|
580
|
+
if (legacyState !== null) {
|
|
581
|
+
for (const v of [
|
|
582
|
+
legacyState.currentVersion,
|
|
583
|
+
legacyState.pendingBoot?.version ?? null,
|
|
584
|
+
legacyState.previousVersion
|
|
585
|
+
]) {
|
|
586
|
+
if (typeof v === "string" && !candidates.includes(v)) candidates.push(v);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
try {
|
|
590
|
+
for (const entry of fs3.readdirSync(versionsDir(rootDir))) {
|
|
591
|
+
if (!entry.startsWith(".") && !candidates.includes(entry)) candidates.push(entry);
|
|
592
|
+
}
|
|
593
|
+
} catch {
|
|
594
|
+
}
|
|
595
|
+
const source = candidates.find((v) => validateClosureDir(versionDir(rootDir, v), nodeMajor, spec, v) === null);
|
|
596
|
+
if (source === void 0) {
|
|
597
|
+
return {
|
|
598
|
+
migrated: false,
|
|
599
|
+
version: null,
|
|
600
|
+
reason: "no valid legacy versions/<X> to adopt"
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
if (fs3.existsSync(dest)) {
|
|
604
|
+
const aside = path3.join(rootDir, `.trash-${now()}-${process.pid}-stale-current`);
|
|
605
|
+
try {
|
|
606
|
+
fs3.renameSync(dest, aside);
|
|
607
|
+
} catch {
|
|
608
|
+
rmrf(dest);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
try {
|
|
612
|
+
fs3.renameSync(versionDir(rootDir, source), dest);
|
|
613
|
+
} catch (err) {
|
|
614
|
+
log(`[single-copy] migration rename of versions/${source} failed (${errMsg(err)})`);
|
|
615
|
+
return {
|
|
616
|
+
migrated: false,
|
|
617
|
+
version: null,
|
|
618
|
+
reason: errMsg(err)
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
rmrf(versionsDir(rootDir));
|
|
622
|
+
rmrf(stateFilePath(rootDir));
|
|
623
|
+
const dataDir = path3.dirname(rootDir);
|
|
624
|
+
for (const marker of LEGACY_FRAMEWORK_MARKERS) rmrf(path3.join(dataDir, marker));
|
|
625
|
+
log(`[single-copy] migrated legacy versions/${source} \u2192 current`);
|
|
626
|
+
return {
|
|
627
|
+
migrated: true,
|
|
628
|
+
version: source,
|
|
629
|
+
reason: "adopted legacy version"
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
__name(migrateToSingleCopy, "migrateToSingleCopy");
|
|
633
|
+
function sweepTransientRootDirs(rootDir, now, staleMs) {
|
|
634
|
+
let entries;
|
|
635
|
+
try {
|
|
636
|
+
entries = fs3.readdirSync(rootDir);
|
|
637
|
+
} catch {
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
for (const entry of entries) {
|
|
641
|
+
if (!entry.startsWith(".staging-") && !entry.startsWith(".trash-")) continue;
|
|
642
|
+
const full = path3.join(rootDir, entry);
|
|
643
|
+
try {
|
|
644
|
+
if (now - fs3.statSync(full).mtimeMs < staleMs) continue;
|
|
645
|
+
} catch {
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
rmrf(full);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
__name(sweepTransientRootDirs, "sweepTransientRootDirs");
|
|
652
|
+
function planBoot(currentValid) {
|
|
653
|
+
if (currentValid) {
|
|
654
|
+
return {
|
|
655
|
+
kind: "current",
|
|
656
|
+
reason: "single-copy closure present and valid"
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
return {
|
|
660
|
+
kind: "baked",
|
|
661
|
+
reason: "no valid single-copy closure \u2014 booting the baked seed"
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
__name(planBoot, "planBoot");
|
|
355
665
|
function parse(version) {
|
|
356
666
|
const dashIdx = version.indexOf("-");
|
|
357
667
|
const base = dashIdx === -1 ? version : version.slice(0, dashIdx);
|
|
@@ -384,142 +694,27 @@ var require_dist = __commonJS({
|
|
|
384
694
|
return 0;
|
|
385
695
|
}
|
|
386
696
|
__name(compareSemver, "compareSemver");
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
return {
|
|
390
|
-
kind: "baked",
|
|
391
|
-
reason: "no server-root state",
|
|
392
|
-
stateToWrite: null
|
|
393
|
-
};
|
|
394
|
-
}
|
|
395
|
-
let next = state;
|
|
396
|
-
let changed = false;
|
|
397
|
-
if (next.pendingBoot !== null) {
|
|
398
|
-
const pending = next.pendingBoot;
|
|
399
|
-
if (pending.bootAttempts >= 1) {
|
|
400
|
-
next = {
|
|
401
|
-
...next,
|
|
402
|
-
pendingBoot: null,
|
|
403
|
-
rolledBack: {
|
|
404
|
-
fromVersion: pending.version,
|
|
405
|
-
toVersion: next.currentVersion,
|
|
406
|
-
atMs: now(),
|
|
407
|
-
reason: "probation boot did not reach ready \u2014 rolled back"
|
|
408
|
-
}
|
|
409
|
-
};
|
|
410
|
-
changed = true;
|
|
411
|
-
} else if (!isValidVersion(pending.version)) {
|
|
412
|
-
next = {
|
|
413
|
-
...next,
|
|
414
|
-
pendingBoot: null,
|
|
415
|
-
rolledBack: {
|
|
416
|
-
fromVersion: pending.version,
|
|
417
|
-
toVersion: next.currentVersion,
|
|
418
|
-
atMs: now(),
|
|
419
|
-
reason: "staged version failed validation"
|
|
420
|
-
}
|
|
421
|
-
};
|
|
422
|
-
changed = true;
|
|
423
|
-
} else {
|
|
424
|
-
return {
|
|
425
|
-
kind: "data-root",
|
|
426
|
-
version: pending.version,
|
|
427
|
-
probation: true,
|
|
428
|
-
stateToWrite: {
|
|
429
|
-
...next,
|
|
430
|
-
pendingBoot: {
|
|
431
|
-
...pending,
|
|
432
|
-
bootAttempts: pending.bootAttempts + 1
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
if (next.currentVersion !== null) {
|
|
439
|
-
if (isValidVersion(next.currentVersion)) {
|
|
440
|
-
if (!changed && seedVersion !== null && compareSemver(seedVersion, next.currentVersion) > 0) {
|
|
441
|
-
return {
|
|
442
|
-
kind: "baked",
|
|
443
|
-
reason: `baked seed ${seedVersion} is newer than active data-root ${next.currentVersion} \u2014 adopting the image seed`,
|
|
444
|
-
stateToWrite: {
|
|
445
|
-
...next,
|
|
446
|
-
currentVersion: null,
|
|
447
|
-
previousVersion: null,
|
|
448
|
-
pendingBoot: null,
|
|
449
|
-
rolledBack: null
|
|
450
|
-
}
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
return {
|
|
454
|
-
kind: "data-root",
|
|
455
|
-
version: next.currentVersion,
|
|
456
|
-
probation: false,
|
|
457
|
-
stateToWrite: changed ? next : null
|
|
458
|
-
};
|
|
459
|
-
}
|
|
460
|
-
const broken = next.currentVersion;
|
|
461
|
-
if (next.previousVersion !== null && isValidVersion(next.previousVersion)) {
|
|
462
|
-
const fallback = next.previousVersion;
|
|
463
|
-
return {
|
|
464
|
-
kind: "data-root",
|
|
465
|
-
version: fallback,
|
|
466
|
-
probation: false,
|
|
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
|
-
}
|
|
478
|
-
};
|
|
479
|
-
}
|
|
480
|
-
return {
|
|
481
|
-
kind: "baked",
|
|
482
|
-
reason: `active version ${broken} failed validation and no valid previous version exists`,
|
|
483
|
-
stateToWrite: {
|
|
484
|
-
...next,
|
|
485
|
-
currentVersion: null,
|
|
486
|
-
rolledBack: {
|
|
487
|
-
fromVersion: broken,
|
|
488
|
-
toVersion: null,
|
|
489
|
-
atMs: now(),
|
|
490
|
-
reason: "active version failed validation"
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
};
|
|
494
|
-
}
|
|
495
|
-
return {
|
|
496
|
-
kind: "baked",
|
|
497
|
-
reason: "no active data-root version",
|
|
498
|
-
stateToWrite: changed ? next : null
|
|
499
|
-
};
|
|
500
|
-
}
|
|
501
|
-
__name(planBoot, "planBoot");
|
|
502
|
-
var fs3 = __toESM2(require("fs"));
|
|
503
|
-
var path3 = __toESM2(require("path"));
|
|
697
|
+
var fs4 = __toESM2(require("fs"));
|
|
698
|
+
var path4 = __toESM2(require("path"));
|
|
504
699
|
function detectWorkspaceRoot(fromDir) {
|
|
505
|
-
let dir =
|
|
700
|
+
let dir = path4.resolve(fromDir);
|
|
506
701
|
for (; ; ) {
|
|
507
|
-
const pkgPath =
|
|
702
|
+
const pkgPath = path4.join(dir, "package.json");
|
|
508
703
|
try {
|
|
509
|
-
const raw = JSON.parse(
|
|
704
|
+
const raw = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
|
|
510
705
|
if (typeof raw === "object" && raw !== null && raw["workspaces"] !== void 0) {
|
|
511
706
|
return dir;
|
|
512
707
|
}
|
|
513
708
|
} catch {
|
|
514
709
|
}
|
|
515
|
-
const parent =
|
|
710
|
+
const parent = path4.dirname(dir);
|
|
516
711
|
if (parent === dir) return null;
|
|
517
712
|
dir = parent;
|
|
518
713
|
}
|
|
519
714
|
}
|
|
520
715
|
__name(detectWorkspaceRoot, "detectWorkspaceRoot");
|
|
521
|
-
var
|
|
522
|
-
var
|
|
716
|
+
var fs5 = __toESM2(require("fs"));
|
|
717
|
+
var path5 = __toESM2(require("path"));
|
|
523
718
|
var import_node_url = require("url");
|
|
524
719
|
var HOST_EXTERNAL_SPECIFIERS = [
|
|
525
720
|
"@camstack/system",
|
|
@@ -543,7 +738,7 @@ var require_dist = __commonJS({
|
|
|
543
738
|
console.warn("[starter] module.registerHooks unavailable (Node < 22.15) \u2014 host-external redirect skipped");
|
|
544
739
|
return;
|
|
545
740
|
}
|
|
546
|
-
const anchorURL = (0, import_node_url.pathToFileURL)(
|
|
741
|
+
const anchorURL = (0, import_node_url.pathToFileURL)(path5.join(activeRootDir, "node_modules", "__camstack_starter_anchor__.js")).href;
|
|
547
742
|
const hooks = {
|
|
548
743
|
resolve: /* @__PURE__ */ __name((specifier, context, nextResolve) => {
|
|
549
744
|
if (isHostExternal(specifier)) {
|
|
@@ -561,24 +756,23 @@ var require_dist = __commonJS({
|
|
|
561
756
|
registerHooks(hooks);
|
|
562
757
|
}
|
|
563
758
|
__name(registerActiveRootResolver, "registerActiveRootResolver");
|
|
564
|
-
function
|
|
759
|
+
function readClosureVersion(closureDir, spec) {
|
|
565
760
|
try {
|
|
566
|
-
const
|
|
567
|
-
const raw = JSON.parse(fs4.readFileSync(pkgJsonPath, "utf-8"));
|
|
761
|
+
const raw = JSON.parse(fs5.readFileSync(path5.join(rootPackageDir2(closureDir, spec), "package.json"), "utf-8"));
|
|
568
762
|
if (typeof raw === "object" && raw !== null) {
|
|
569
763
|
const version = raw["version"];
|
|
570
764
|
if (typeof version === "string") return version;
|
|
571
765
|
}
|
|
572
|
-
return null;
|
|
573
766
|
} catch {
|
|
574
|
-
return null;
|
|
575
767
|
}
|
|
768
|
+
return null;
|
|
576
769
|
}
|
|
577
|
-
__name(
|
|
770
|
+
__name(readClosureVersion, "readClosureVersion");
|
|
578
771
|
function runNodeStarter(options) {
|
|
579
772
|
const env = options.env ?? process.env;
|
|
580
773
|
const now = options.now ?? Date.now;
|
|
581
774
|
const registerResolver = options.registerResolver ?? registerActiveRootResolver;
|
|
775
|
+
const nodeMajor = options.nodeMajor ?? Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
|
|
582
776
|
const disabled = env[options.envNames.killSwitch] === "off";
|
|
583
777
|
const workspaceRoot = detectWorkspaceRoot(options.starterDir);
|
|
584
778
|
if (workspaceRoot !== null) {
|
|
@@ -594,36 +788,24 @@ var require_dist = __commonJS({
|
|
|
594
788
|
return;
|
|
595
789
|
}
|
|
596
790
|
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
|
-
}
|
|
791
|
+
const swap = applyPendingRootSwap(rootDir, options.spec, nodeMajor, now, (m) => console.log(m));
|
|
792
|
+
if (swap.applied) console.log(`[starter] applied staged update \u2192 ${swap.version ?? "?"}`);
|
|
793
|
+
else if (swap.reason !== "no pending swap") console.warn(`[starter] staged update NOT applied: ${swap.reason}`);
|
|
794
|
+
const migration = migrateToSingleCopy(rootDir, options.spec, nodeMajor, now, (m) => console.log(m));
|
|
795
|
+
if (migration.migrated) console.log(`[starter] migrated legacy layout \u2192 single copy (${migration.version ?? "?"})`);
|
|
796
|
+
const activeRootDir = currentDir(rootDir);
|
|
797
|
+
const invalid = validateClosureDir(activeRootDir, nodeMajor, options.spec);
|
|
798
|
+
if (invalid !== null && fs5.existsSync(activeRootDir)) {
|
|
799
|
+
console.warn(`[starter] single copy invalid: ${invalid}`);
|
|
619
800
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
const
|
|
623
|
-
|
|
801
|
+
const plan = planBoot(invalid === null);
|
|
802
|
+
if (plan.kind === "current") {
|
|
803
|
+
const version = readClosureVersion(activeRootDir, options.spec);
|
|
804
|
+
const entry = currentEntryPath(rootDir, options.spec);
|
|
805
|
+
console.log(`[starter] booting ${options.spec.packageName}@${version ?? "?"} from ${activeRootDir}`);
|
|
624
806
|
env[options.envNames.bootMode] = "data-root";
|
|
625
807
|
env[options.envNames.activeRoot] = activeRootDir;
|
|
626
|
-
env[options.envNames.activeVersion] =
|
|
808
|
+
if (version !== null) env[options.envNames.activeVersion] = version;
|
|
627
809
|
registerResolver(activeRootDir);
|
|
628
810
|
options.loadEntry(entry);
|
|
629
811
|
return;
|
|
@@ -634,8 +816,8 @@ var require_dist = __commonJS({
|
|
|
634
816
|
}
|
|
635
817
|
__name(runNodeStarter, "runNodeStarter");
|
|
636
818
|
var import_node_child_process = require("child_process");
|
|
637
|
-
var
|
|
638
|
-
var
|
|
819
|
+
var fs6 = __toESM2(require("fs"));
|
|
820
|
+
var path6 = __toESM2(require("path"));
|
|
639
821
|
var import_node_util = require("util");
|
|
640
822
|
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
641
823
|
function buildNpmRegistryArgs(registry) {
|
|
@@ -650,9 +832,10 @@ var require_dist = __commonJS({
|
|
|
650
832
|
var NPM_VIEW_TIMEOUT_MS = 2e4;
|
|
651
833
|
var NPM_INSTALL_TIMEOUT_MS = 15 * 6e4;
|
|
652
834
|
var RESTART_REASON_PREFIX = "server-update";
|
|
835
|
+
var STALE_TRANSIENT_MS = 24 * 60 * 60 * 1e3;
|
|
653
836
|
function readPackageVersion(pkgJsonPath) {
|
|
654
837
|
try {
|
|
655
|
-
const raw = JSON.parse(
|
|
838
|
+
const raw = JSON.parse(fs6.readFileSync(pkgJsonPath, "utf-8"));
|
|
656
839
|
if (typeof raw === "object" && raw !== null) {
|
|
657
840
|
const version = raw["version"];
|
|
658
841
|
if (typeof version === "string") return version;
|
|
@@ -662,9 +845,9 @@ var require_dist = __commonJS({
|
|
|
662
845
|
return null;
|
|
663
846
|
}
|
|
664
847
|
__name(readPackageVersion, "readPackageVersion");
|
|
665
|
-
var RootUpdateService = class
|
|
848
|
+
var RootUpdateService = class {
|
|
666
849
|
static {
|
|
667
|
-
__name(this, "
|
|
850
|
+
__name(this, "RootUpdateService");
|
|
668
851
|
}
|
|
669
852
|
spec;
|
|
670
853
|
envNames;
|
|
@@ -683,7 +866,7 @@ var require_dist = __commonJS({
|
|
|
683
866
|
this.envNames = options.envNames;
|
|
684
867
|
this.logger = options.logger;
|
|
685
868
|
this.restartServerFn = options.restartServer;
|
|
686
|
-
this.dataDir =
|
|
869
|
+
this.dataDir = path6.resolve(options.dataDir);
|
|
687
870
|
this.execNpm = options.execNpm ?? (async (args, opts) => {
|
|
688
871
|
const { stdout } = await execFileAsync("npm", [
|
|
689
872
|
...args
|
|
@@ -719,63 +902,37 @@ var require_dist = __commonJS({
|
|
|
719
902
|
seedVersion() {
|
|
720
903
|
const seedDir = this.env[this.envNames.seedDir];
|
|
721
904
|
if (seedDir === void 0 || seedDir.length === 0) return null;
|
|
722
|
-
return readPackageVersion(
|
|
905
|
+
return readPackageVersion(path6.join(seedDir, "package.json"));
|
|
723
906
|
}
|
|
724
907
|
rootDir() {
|
|
725
908
|
return serverRootDir(this.dataDir);
|
|
726
909
|
}
|
|
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) {
|
|
910
|
+
updateState() {
|
|
749
911
|
if (this.inFlight === "checking") return "checking";
|
|
750
912
|
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
|
-
}
|
|
913
|
+
if (readPendingRootSwap(this.rootDir()) !== null) return "pending-restart";
|
|
758
914
|
return "idle";
|
|
759
915
|
}
|
|
760
916
|
async getServerPackageStatus() {
|
|
761
|
-
const { state, corrupt } = this.readStateInfo();
|
|
762
917
|
const runningVersion = this.runningVersion();
|
|
763
918
|
const latestVersion = this.checkCache?.latestVersion ?? null;
|
|
919
|
+
const pending = readPendingRootSwap(this.rootDir());
|
|
764
920
|
const updateAvailable = latestVersion !== null && runningVersion !== null && compareSemver(latestVersion, runningVersion) > 0;
|
|
765
921
|
return {
|
|
766
922
|
packageName: this.spec.packageName,
|
|
767
923
|
runningVersion,
|
|
768
924
|
nodeRuntimeVersion: process.versions.node,
|
|
769
|
-
activeVersion: this.env[this.envNames.activeVersion] ??
|
|
770
|
-
|
|
925
|
+
activeVersion: this.env[this.envNames.activeVersion] ?? runningVersion,
|
|
926
|
+
// Single-copy model: no N-1 retention, no rollback record.
|
|
927
|
+
previousVersion: null,
|
|
771
928
|
seedVersion: this.seedVersion(),
|
|
772
929
|
latestVersion,
|
|
773
930
|
updateAvailable,
|
|
774
931
|
bootMode: this.resolveBootMode(),
|
|
775
|
-
updateState: this.updateState(
|
|
776
|
-
pendingVersion:
|
|
777
|
-
rolledBack:
|
|
778
|
-
stateFileCorrupt:
|
|
932
|
+
updateState: this.updateState(),
|
|
933
|
+
pendingVersion: pending?.version ?? null,
|
|
934
|
+
rolledBack: null,
|
|
935
|
+
stateFileCorrupt: false,
|
|
779
936
|
lastCheckedAtMs: this.checkCache?.checkedAtMs ?? null
|
|
780
937
|
};
|
|
781
938
|
}
|
|
@@ -862,13 +1019,13 @@ var require_dist = __commonJS({
|
|
|
862
1019
|
message: `Refused: another operation is in flight (${this.inFlight}).`
|
|
863
1020
|
};
|
|
864
1021
|
}
|
|
865
|
-
const
|
|
866
|
-
if (
|
|
1022
|
+
const pendingBefore = readPendingRootSwap(this.rootDir());
|
|
1023
|
+
if (pendingBefore !== null) {
|
|
867
1024
|
return {
|
|
868
1025
|
accepted: false,
|
|
869
1026
|
targetVersion: input.version ?? null,
|
|
870
1027
|
restarting: false,
|
|
871
|
-
message: `Refused: version ${
|
|
1028
|
+
message: `Refused: version ${pendingBefore.version} is already staged and awaiting restart.`
|
|
872
1029
|
};
|
|
873
1030
|
}
|
|
874
1031
|
let target = input.version ?? null;
|
|
@@ -895,7 +1052,7 @@ var require_dist = __commonJS({
|
|
|
895
1052
|
}
|
|
896
1053
|
if (isDevChannelVersion(target)) {
|
|
897
1054
|
const uploadsDir = devUploadVersionDir(this.rootDir(), target);
|
|
898
|
-
if (!
|
|
1055
|
+
if (!fs6.existsSync(uploadsDir)) {
|
|
899
1056
|
return {
|
|
900
1057
|
accepted: false,
|
|
901
1058
|
targetVersion: target,
|
|
@@ -905,8 +1062,9 @@ var require_dist = __commonJS({
|
|
|
905
1062
|
}
|
|
906
1063
|
}
|
|
907
1064
|
this.inFlight = "staging";
|
|
1065
|
+
let stagingPath;
|
|
908
1066
|
try {
|
|
909
|
-
await this.
|
|
1067
|
+
stagingPath = await this.stageClosure(target);
|
|
910
1068
|
} catch (err) {
|
|
911
1069
|
const message = err instanceof Error ? err.message : String(err);
|
|
912
1070
|
this.logger.error("root package update staging failed", {
|
|
@@ -924,21 +1082,16 @@ var require_dist = __commonJS({
|
|
|
924
1082
|
} finally {
|
|
925
1083
|
this.inFlight = "idle";
|
|
926
1084
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
fromVersion: state.currentVersion,
|
|
933
|
-
requestedAtMs: this.now(),
|
|
934
|
-
bootAttempts: 0
|
|
935
|
-
},
|
|
936
|
-
rolledBack: null
|
|
1085
|
+
writePendingRootSwap(this.rootDir(), {
|
|
1086
|
+
schemaVersion: 1,
|
|
1087
|
+
version: target,
|
|
1088
|
+
stagingPath,
|
|
1089
|
+
requestedAtMs: this.now()
|
|
937
1090
|
});
|
|
938
1091
|
this.logger.info("root package update staged \u2014 restarting to apply", {
|
|
939
1092
|
meta: {
|
|
940
1093
|
targetVersion: target,
|
|
941
|
-
fromVersion:
|
|
1094
|
+
fromVersion: runningVersion
|
|
942
1095
|
}
|
|
943
1096
|
});
|
|
944
1097
|
this.restartServerFn(`${RESTART_REASON_PREFIX}: ${this.spec.packageName}@${target}`);
|
|
@@ -946,21 +1099,23 @@ var require_dist = __commonJS({
|
|
|
946
1099
|
accepted: true,
|
|
947
1100
|
targetVersion: target,
|
|
948
1101
|
restarting: true,
|
|
949
|
-
message: `Staged ${this.spec.packageName}@${target} \u2014 restarting to apply (
|
|
1102
|
+
message: `Staged ${this.spec.packageName}@${target} \u2014 restarting to apply (single automatic reboot).`
|
|
950
1103
|
};
|
|
951
1104
|
}
|
|
952
1105
|
/**
|
|
953
|
-
* npm-install the target closure into a SAME-FS staging dir
|
|
954
|
-
*
|
|
1106
|
+
* npm-install the target closure into a SAME-FS `.staging-*` dir, then
|
|
1107
|
+
* validate it (entry + version + native prebuilds). Returns the staging dir
|
|
1108
|
+
* path for the pending-root-swap marker; the STARTER swaps it into `current`
|
|
1109
|
+
* on the next boot. Throws (cleaning up the staging dir) on any failure so
|
|
1110
|
+
* `applyServerUpdate` reports a staging error and never arms a bad swap.
|
|
955
1111
|
*/
|
|
956
|
-
async
|
|
1112
|
+
async stageClosure(target) {
|
|
957
1113
|
const rootDir = this.rootDir();
|
|
958
|
-
|
|
959
|
-
fs5.mkdirSync(vDir, {
|
|
1114
|
+
fs6.mkdirSync(rootDir, {
|
|
960
1115
|
recursive: true
|
|
961
1116
|
});
|
|
962
|
-
const stagingDir =
|
|
963
|
-
|
|
1117
|
+
const stagingDir = stagingDirPath(rootDir, target, process.pid, this.now());
|
|
1118
|
+
fs6.mkdirSync(stagingDir, {
|
|
964
1119
|
recursive: true
|
|
965
1120
|
});
|
|
966
1121
|
try {
|
|
@@ -973,8 +1128,8 @@ var require_dist = __commonJS({
|
|
|
973
1128
|
"--no-fund",
|
|
974
1129
|
"--loglevel=error"
|
|
975
1130
|
];
|
|
976
|
-
if (
|
|
977
|
-
|
|
1131
|
+
if (fs6.existsSync(devDir)) {
|
|
1132
|
+
fs6.writeFileSync(path6.join(stagingDir, "package.json"), JSON.stringify(this.buildDevUploadsPackageJson(devDir, target), null, 2), "utf-8");
|
|
978
1133
|
await this.execNpm([
|
|
979
1134
|
...installArgs,
|
|
980
1135
|
...buildNpmRegistryArgs(registry)
|
|
@@ -983,7 +1138,7 @@ var require_dist = __commonJS({
|
|
|
983
1138
|
timeoutMs: NPM_INSTALL_TIMEOUT_MS
|
|
984
1139
|
});
|
|
985
1140
|
} else {
|
|
986
|
-
|
|
1141
|
+
fs6.writeFileSync(path6.join(stagingDir, "package.json"), JSON.stringify({
|
|
987
1142
|
name: "camstack-node-root",
|
|
988
1143
|
private: true
|
|
989
1144
|
}, null, 2), "utf-8");
|
|
@@ -997,25 +1152,21 @@ var require_dist = __commonJS({
|
|
|
997
1152
|
});
|
|
998
1153
|
}
|
|
999
1154
|
const entry = rootEntryPath2(stagingDir, this.spec);
|
|
1000
|
-
if (!
|
|
1155
|
+
if (!fs6.existsSync(entry)) {
|
|
1001
1156
|
throw new Error(`staged closure is missing the root entry (${entry})`);
|
|
1002
1157
|
}
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1005
|
-
|
|
1158
|
+
const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
|
|
1159
|
+
const invalid = validateClosureDir(stagingDir, nodeMajor, this.spec, target);
|
|
1160
|
+
if (invalid !== null) {
|
|
1161
|
+
throw new Error(`staged closure invalid: ${invalid}`);
|
|
1006
1162
|
}
|
|
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);
|
|
1163
|
+
const missingNatives = findMissingNativePrebuilds(stagingDir);
|
|
1164
|
+
if (missingNatives.length > 0) {
|
|
1165
|
+
throw new Error(`staged closure missing native prebuilds: ${missingNatives.join(", ")} \u2014 refusing to arm a swap that would break the forked runners`);
|
|
1015
1166
|
}
|
|
1016
|
-
|
|
1167
|
+
return stagingDir;
|
|
1017
1168
|
} catch (err) {
|
|
1018
|
-
await
|
|
1169
|
+
await fs6.promises.rm(stagingDir, {
|
|
1019
1170
|
recursive: true,
|
|
1020
1171
|
force: true
|
|
1021
1172
|
}).catch(() => void 0);
|
|
@@ -1025,11 +1176,9 @@ var require_dist = __commonJS({
|
|
|
1025
1176
|
/**
|
|
1026
1177
|
* Synthetic staging package.json for the dev server-deploy channel:
|
|
1027
1178
|
* `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.
|
|
1179
|
+
* tgz; `overrides` maps EVERY OTHER uploaded `@camstack/<name>` to its tgz.
|
|
1030
1180
|
* 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).
|
|
1181
|
+
* version, a missing root package, or a listed-but-absent tgz all throw.
|
|
1033
1182
|
*/
|
|
1034
1183
|
buildDevUploadsPackageJson(devDir, target) {
|
|
1035
1184
|
const manifest = readDevUploadManifest(devDir);
|
|
@@ -1044,8 +1193,8 @@ var require_dist = __commonJS({
|
|
|
1044
1193
|
throw new Error(`dev-uploads for ${target} do not include the root package ${this.spec.packageName}`);
|
|
1045
1194
|
}
|
|
1046
1195
|
const fileRef = /* @__PURE__ */ __name((filename) => {
|
|
1047
|
-
const abs =
|
|
1048
|
-
if (!
|
|
1196
|
+
const abs = path6.join(devDir, filename);
|
|
1197
|
+
if (!fs6.existsSync(abs)) {
|
|
1049
1198
|
throw new Error(`dev-uploads tarball listed in the manifest is missing: ${abs}`);
|
|
1050
1199
|
}
|
|
1051
1200
|
return `file:${abs}`;
|
|
@@ -1066,73 +1215,28 @@ var require_dist = __commonJS({
|
|
|
1066
1215
|
} : {}
|
|
1067
1216
|
};
|
|
1068
1217
|
}
|
|
1069
|
-
// ── Rollback
|
|
1218
|
+
// ── Rollback (removed — single-copy has no N-1) ────────────────────────
|
|
1219
|
+
/**
|
|
1220
|
+
* Rollback is NOT supported in the single-copy model — there is no retained
|
|
1221
|
+
* N-1 copy to revert to (operator's accepted tradeoff). Recovery is manual:
|
|
1222
|
+
* re-apply a known-good version via {@link applyServerUpdate}, or reinstall
|
|
1223
|
+
* the image seed. Kept on the surface so the cap contract is unchanged.
|
|
1224
|
+
*/
|
|
1070
1225
|
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
1226
|
return {
|
|
1123
|
-
accepted:
|
|
1124
|
-
targetVersion:
|
|
1125
|
-
restarting:
|
|
1126
|
-
message:
|
|
1227
|
+
accepted: false,
|
|
1228
|
+
targetVersion: null,
|
|
1229
|
+
restarting: false,
|
|
1230
|
+
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
1231
|
};
|
|
1128
1232
|
}
|
|
1129
1233
|
// ── Plain restart ─────────────────────────────────────────────────────
|
|
1130
1234
|
/**
|
|
1131
|
-
* Plain process restart — no version change.
|
|
1132
|
-
*
|
|
1133
|
-
*
|
|
1134
|
-
*
|
|
1135
|
-
*
|
|
1235
|
+
* Plain process restart — no version change. Refuse while a stage is in
|
|
1236
|
+
* flight (a concurrent npm install must not be interrupted) or while a
|
|
1237
|
+
* version is already staged awaiting restart (a naive bounce would boot the
|
|
1238
|
+
* OLD version and orphan the pending swap — the operator should apply
|
|
1239
|
+
* instead).
|
|
1136
1240
|
*/
|
|
1137
1241
|
restartNode() {
|
|
1138
1242
|
const runningVersion = this.runningVersion();
|
|
@@ -1144,13 +1248,12 @@ var require_dist = __commonJS({
|
|
|
1144
1248
|
message: `Refused: another operation is in flight (${this.inFlight}).`
|
|
1145
1249
|
};
|
|
1146
1250
|
}
|
|
1147
|
-
|
|
1148
|
-
if (state.pendingBoot !== null) {
|
|
1251
|
+
if (readPendingRootSwap(this.rootDir()) !== null) {
|
|
1149
1252
|
return {
|
|
1150
1253
|
accepted: false,
|
|
1151
1254
|
targetVersion: runningVersion,
|
|
1152
1255
|
restarting: false,
|
|
1153
|
-
message:
|
|
1256
|
+
message: "Refused: a version is staged and awaiting restart \u2014 apply it instead of a plain restart."
|
|
1154
1257
|
};
|
|
1155
1258
|
}
|
|
1156
1259
|
this.logger.info("plain node restart requested", {
|
|
@@ -1168,118 +1271,37 @@ var require_dist = __commonJS({
|
|
|
1168
1271
|
}
|
|
1169
1272
|
// ── Boot health confirmation ──────────────────────────────────────────
|
|
1170
1273
|
/**
|
|
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.
|
|
1274
|
+
* Called by the post-boot path once the node is READY. In the single-copy
|
|
1275
|
+
* model there is no pending version to promote (the swap already happened in
|
|
1276
|
+
* the starter). This becomes a best-effort GC of orphaned transient dirs
|
|
1277
|
+
* (`.staging-*` / `.trash-*` left by a crash mid-swap) + the dev-uploads
|
|
1278
|
+
* sweep. Kept returning the legacy `{ promoted }` shape for the callers.
|
|
1177
1279
|
*/
|
|
1178
1280
|
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"));
|
|
1281
|
+
sweepTransientRootDirs(this.rootDir(), this.now(), STALE_TRANSIENT_MS);
|
|
1201
1282
|
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
1283
|
return {
|
|
1209
|
-
promoted:
|
|
1284
|
+
promoted: null
|
|
1210
1285
|
};
|
|
1211
1286
|
}
|
|
1212
1287
|
/**
|
|
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
1288
|
* Promote-time sweep of `server-root/dev-uploads/`: keep only the
|
|
1264
1289
|
* `keepCount` most recent entries (ordered by the self-describing
|
|
1265
1290
|
* `-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
1291
|
*/
|
|
1270
1292
|
sweepDevUploads(keepCount) {
|
|
1271
1293
|
const dir = devUploadsDir(this.rootDir());
|
|
1272
1294
|
let entries;
|
|
1273
1295
|
try {
|
|
1274
|
-
entries =
|
|
1296
|
+
entries = fs6.readdirSync(dir);
|
|
1275
1297
|
} catch {
|
|
1276
1298
|
return;
|
|
1277
1299
|
}
|
|
1278
|
-
const graveyard =
|
|
1300
|
+
const graveyard = path6.join(dir, ".sweeping");
|
|
1279
1301
|
try {
|
|
1280
|
-
for (const residue of
|
|
1302
|
+
for (const residue of fs6.readdirSync(graveyard)) {
|
|
1281
1303
|
try {
|
|
1282
|
-
|
|
1304
|
+
fs6.rmSync(path6.join(graveyard, residue), {
|
|
1283
1305
|
recursive: true,
|
|
1284
1306
|
force: true
|
|
1285
1307
|
});
|
|
@@ -1294,13 +1316,13 @@ var require_dist = __commonJS({
|
|
|
1294
1316
|
].sort((a, b) => (devChannelEpoch(b) ?? -1) - (devChannelEpoch(a) ?? -1));
|
|
1295
1317
|
const doomed = byNewestFirst.slice(keepCount);
|
|
1296
1318
|
if (doomed.length === 0) return;
|
|
1297
|
-
|
|
1319
|
+
fs6.mkdirSync(graveyard, {
|
|
1298
1320
|
recursive: true
|
|
1299
1321
|
});
|
|
1300
1322
|
for (const entry of doomed) {
|
|
1301
|
-
const aside =
|
|
1323
|
+
const aside = path6.join(graveyard, `${entry}-${this.now()}`);
|
|
1302
1324
|
try {
|
|
1303
|
-
|
|
1325
|
+
fs6.renameSync(path6.join(dir, entry), aside);
|
|
1304
1326
|
} catch (err) {
|
|
1305
1327
|
this.logger.warn("failed to move dev-uploads entry aside for sweep", {
|
|
1306
1328
|
meta: {
|
|
@@ -1311,7 +1333,7 @@ var require_dist = __commonJS({
|
|
|
1311
1333
|
continue;
|
|
1312
1334
|
}
|
|
1313
1335
|
try {
|
|
1314
|
-
|
|
1336
|
+
fs6.rmSync(aside, {
|
|
1315
1337
|
recursive: true,
|
|
1316
1338
|
force: true
|
|
1317
1339
|
});
|
|
@@ -1324,8 +1346,6 @@ var require_dist = __commonJS({
|
|
|
1324
1346
|
}
|
|
1325
1347
|
}
|
|
1326
1348
|
}
|
|
1327
|
-
/** Transient (.staging- / .evicted-) dirs younger than this are never pruned. */
|
|
1328
|
-
static STALE_TRANSIENT_MS = 24 * 60 * 60 * 1e3;
|
|
1329
1349
|
};
|
|
1330
1350
|
}
|
|
1331
1351
|
});
|