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