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