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