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