@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
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,11 @@ 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"));
|
|
298
316
|
function parse(version) {
|
|
299
317
|
const dashIdx = version.indexOf("-");
|
|
300
318
|
const base = dashIdx === -1 ? version : version.slice(0, dashIdx);
|
|
@@ -322,133 +340,268 @@ var require_dist = __commonJS({
|
|
|
322
340
|
if (pa.prerelease > pb.prerelease) return 1;
|
|
323
341
|
return 0;
|
|
324
342
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
343
|
+
var CURRENT_DIRNAME = "current";
|
|
344
|
+
var PENDING_ROOT_SWAP_FILE = ".pending-root-swap.json";
|
|
345
|
+
var LEGACY_FRAMEWORK_MARKERS = [
|
|
346
|
+
".pending-framework-swap.json",
|
|
347
|
+
".framework-swap-confirm.json"
|
|
348
|
+
];
|
|
349
|
+
function currentDir(rootDir) {
|
|
350
|
+
return path3.join(rootDir, CURRENT_DIRNAME);
|
|
351
|
+
}
|
|
352
|
+
function currentEntryPath(rootDir, spec) {
|
|
353
|
+
return rootEntryPath(currentDir(rootDir), spec);
|
|
354
|
+
}
|
|
355
|
+
function stagingDirPath(rootDir, target, pid, now) {
|
|
356
|
+
return path3.join(rootDir, `.staging-${target}-${pid}-${now}`);
|
|
357
|
+
}
|
|
358
|
+
function pendingRootSwapPath(rootDir) {
|
|
359
|
+
return path3.join(rootDir, PENDING_ROOT_SWAP_FILE);
|
|
360
|
+
}
|
|
361
|
+
function isPendingRootSwap(v) {
|
|
362
|
+
if (typeof v !== "object" || v === null) return false;
|
|
363
|
+
const m = v;
|
|
364
|
+
return m["schemaVersion"] === 1 && typeof m["version"] === "string" && typeof m["stagingPath"] === "string" && typeof m["requestedAtMs"] === "number";
|
|
365
|
+
}
|
|
366
|
+
function readPendingRootSwap(rootDir) {
|
|
367
|
+
try {
|
|
368
|
+
const raw = JSON.parse(fs3.readFileSync(pendingRootSwapPath(rootDir), "utf-8"));
|
|
369
|
+
return isPendingRootSwap(raw) ? raw : null;
|
|
370
|
+
} catch {
|
|
371
|
+
return null;
|
|
328
372
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
}
|
|
373
|
+
}
|
|
374
|
+
function writePendingRootSwap(rootDir, marker) {
|
|
375
|
+
fs3.mkdirSync(rootDir, { recursive: true });
|
|
376
|
+
const target = pendingRootSwapPath(rootDir);
|
|
377
|
+
const tmp = `${target}.tmp`;
|
|
378
|
+
fs3.writeFileSync(tmp, JSON.stringify(marker, null, 2), "utf-8");
|
|
379
|
+
fs3.renameSync(tmp, target);
|
|
380
|
+
}
|
|
381
|
+
function clearPendingRootSwap(rootDir) {
|
|
382
|
+
try {
|
|
383
|
+
fs3.rmSync(pendingRootSwapPath(rootDir), { force: true });
|
|
384
|
+
} catch {
|
|
368
385
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
386
|
+
}
|
|
387
|
+
var REQUIRED_NATIVE_PACKAGES = ["better-sqlite3", "sharp", "node-av"];
|
|
388
|
+
var NATIVE_SEARCH_DIRS = {
|
|
389
|
+
"better-sqlite3": ["better-sqlite3"],
|
|
390
|
+
sharp: ["sharp", "@img"],
|
|
391
|
+
"node-av": ["node-av", "@seydx"]
|
|
392
|
+
};
|
|
393
|
+
function hasDotNode(dir, maxDepth) {
|
|
394
|
+
let entries;
|
|
395
|
+
try {
|
|
396
|
+
entries = fs3.readdirSync(dir, { withFileTypes: true });
|
|
397
|
+
} catch {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
for (const entry of entries) {
|
|
401
|
+
if (entry.isFile() && entry.name.endsWith(".node")) return true;
|
|
402
|
+
}
|
|
403
|
+
if (maxDepth <= 0) return false;
|
|
404
|
+
for (const entry of entries) {
|
|
405
|
+
if (entry.isDirectory() && hasDotNode(path3.join(dir, entry.name), maxDepth - 1)) return true;
|
|
406
|
+
}
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
function findMissingNativePrebuilds(closureDir) {
|
|
410
|
+
const nm = path3.join(closureDir, "node_modules");
|
|
411
|
+
const missing = [];
|
|
412
|
+
for (const pkg of REQUIRED_NATIVE_PACKAGES) {
|
|
413
|
+
const searchDirs = NATIVE_SEARCH_DIRS[pkg] ?? [pkg];
|
|
414
|
+
const found = searchDirs.some((rel) => hasDotNode(path3.join(nm, ...rel.split("/")), 4));
|
|
415
|
+
if (!found) missing.push(pkg);
|
|
416
|
+
}
|
|
417
|
+
return missing;
|
|
418
|
+
}
|
|
419
|
+
function rmrf(target) {
|
|
420
|
+
try {
|
|
421
|
+
fs3.rmSync(target, { recursive: true, force: true });
|
|
422
|
+
} catch {
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function errMsg(err) {
|
|
426
|
+
return err instanceof Error ? err.message : String(err);
|
|
427
|
+
}
|
|
428
|
+
function applyPendingRootSwap(rootDir, spec, nodeMajor, now, log) {
|
|
429
|
+
const marker = readPendingRootSwap(rootDir);
|
|
430
|
+
if (marker === null) return { applied: false, version: null, reason: "no pending swap" };
|
|
431
|
+
const staging = marker.stagingPath;
|
|
432
|
+
const dest = currentDir(rootDir);
|
|
433
|
+
const invalid = validateClosureDir(staging, nodeMajor, spec, marker.version);
|
|
434
|
+
if (invalid !== null) {
|
|
435
|
+
if (!fs3.existsSync(staging) && validateClosureDir(dest, nodeMajor, spec, marker.version) === null) {
|
|
436
|
+
clearPendingRootSwap(rootDir);
|
|
437
|
+
log(`[single-copy] pending swap ${marker.version} already applied \u2014 cleared marker`);
|
|
438
|
+
return { applied: true, version: marker.version, reason: "already applied" };
|
|
410
439
|
}
|
|
440
|
+
log(`[single-copy] staged swap ${marker.version} invalid \u2014 discarding (${invalid})`);
|
|
441
|
+
rmrf(staging);
|
|
442
|
+
clearPendingRootSwap(rootDir);
|
|
443
|
+
return { applied: false, version: null, reason: invalid };
|
|
444
|
+
}
|
|
445
|
+
const missing = findMissingNativePrebuilds(staging);
|
|
446
|
+
if (missing.length > 0) {
|
|
447
|
+
log(
|
|
448
|
+
`[single-copy] staged swap ${marker.version} missing native prebuilds: ${missing.join(", ")} \u2014 discarding`
|
|
449
|
+
);
|
|
450
|
+
rmrf(staging);
|
|
451
|
+
clearPendingRootSwap(rootDir);
|
|
411
452
|
return {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
453
|
+
applied: false,
|
|
454
|
+
version: null,
|
|
455
|
+
reason: `missing native prebuilds: ${missing.join(", ")}`
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
let trash = null;
|
|
459
|
+
try {
|
|
460
|
+
if (fs3.existsSync(dest)) {
|
|
461
|
+
trash = path3.join(rootDir, `.trash-${now()}-${process.pid}`);
|
|
462
|
+
fs3.renameSync(dest, trash);
|
|
463
|
+
}
|
|
464
|
+
try {
|
|
465
|
+
fs3.renameSync(staging, dest);
|
|
466
|
+
} catch (err) {
|
|
467
|
+
if (trash !== null && !fs3.existsSync(dest)) {
|
|
468
|
+
try {
|
|
469
|
+
fs3.renameSync(trash, dest);
|
|
470
|
+
} catch {
|
|
422
471
|
}
|
|
423
472
|
}
|
|
424
|
-
|
|
473
|
+
throw err;
|
|
474
|
+
}
|
|
475
|
+
} catch (err) {
|
|
476
|
+
log(`[single-copy] swap of ${marker.version} FAILED \u2014 current preserved (${errMsg(err)})`);
|
|
477
|
+
return { applied: false, version: null, reason: errMsg(err) };
|
|
425
478
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
};
|
|
479
|
+
if (trash !== null) rmrf(trash);
|
|
480
|
+
clearPendingRootSwap(rootDir);
|
|
481
|
+
log(`[single-copy] applied staged root swap \u2192 ${marker.version}`);
|
|
482
|
+
return { applied: true, version: marker.version, reason: "applied" };
|
|
431
483
|
}
|
|
432
|
-
|
|
433
|
-
|
|
484
|
+
function readClosureVersion(closureDir, spec) {
|
|
485
|
+
try {
|
|
486
|
+
const raw = JSON.parse(
|
|
487
|
+
fs3.readFileSync(path3.join(rootPackageDir(closureDir, spec), "package.json"), "utf-8")
|
|
488
|
+
);
|
|
489
|
+
if (typeof raw === "object" && raw !== null) {
|
|
490
|
+
const version = raw["version"];
|
|
491
|
+
if (typeof version === "string") return version;
|
|
492
|
+
}
|
|
493
|
+
} catch {
|
|
494
|
+
}
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
function isAdoptablePendingClosure(rootDir, version, nodeMajor, spec) {
|
|
498
|
+
const dir = versionDir(rootDir, version);
|
|
499
|
+
return validateClosureDir(dir, nodeMajor, spec, version) === null && findMissingNativePrebuilds(dir).length === 0;
|
|
500
|
+
}
|
|
501
|
+
function migrateToSingleCopy(rootDir, spec, nodeMajor, now, log) {
|
|
502
|
+
const dest = currentDir(rootDir);
|
|
503
|
+
const legacyState = readServerRootState(rootDir);
|
|
504
|
+
const pendingVersion = legacyState?.pendingBoot?.version ?? null;
|
|
505
|
+
const pendingAdoptable = pendingVersion !== null && isAdoptablePendingClosure(rootDir, pendingVersion, nodeMajor, spec);
|
|
506
|
+
const currentValid = fs3.existsSync(dest) && validateClosureDir(dest, nodeMajor, spec) === null;
|
|
507
|
+
if (currentValid) {
|
|
508
|
+
const currentVersion = readClosureVersion(dest, spec);
|
|
509
|
+
const pendingSupersedes = pendingAdoptable && pendingVersion !== null && (currentVersion === null || compareSemver(pendingVersion, currentVersion) > 0);
|
|
510
|
+
if (!pendingSupersedes) {
|
|
511
|
+
return { migrated: false, version: null, reason: "current already present" };
|
|
512
|
+
}
|
|
513
|
+
log(
|
|
514
|
+
`[single-copy] in-flight pending ${pendingVersion} supersedes older current ${currentVersion ?? "?"}`
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
const candidates = [];
|
|
518
|
+
if (pendingAdoptable && pendingVersion !== null) candidates.push(pendingVersion);
|
|
519
|
+
if (legacyState !== null) {
|
|
520
|
+
for (const v of [
|
|
521
|
+
legacyState.currentVersion,
|
|
522
|
+
legacyState.pendingBoot?.version ?? null,
|
|
523
|
+
legacyState.previousVersion
|
|
524
|
+
]) {
|
|
525
|
+
if (typeof v === "string" && !candidates.includes(v)) candidates.push(v);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
try {
|
|
529
|
+
for (const entry of fs3.readdirSync(versionsDir(rootDir))) {
|
|
530
|
+
if (!entry.startsWith(".") && !candidates.includes(entry)) candidates.push(entry);
|
|
531
|
+
}
|
|
532
|
+
} catch {
|
|
533
|
+
}
|
|
534
|
+
const source = candidates.find(
|
|
535
|
+
(v) => validateClosureDir(versionDir(rootDir, v), nodeMajor, spec, v) === null
|
|
536
|
+
);
|
|
537
|
+
if (source === void 0) {
|
|
538
|
+
return { migrated: false, version: null, reason: "no valid legacy versions/<X> to adopt" };
|
|
539
|
+
}
|
|
540
|
+
if (fs3.existsSync(dest)) {
|
|
541
|
+
const aside = path3.join(rootDir, `.trash-${now()}-${process.pid}-stale-current`);
|
|
542
|
+
try {
|
|
543
|
+
fs3.renameSync(dest, aside);
|
|
544
|
+
} catch {
|
|
545
|
+
rmrf(dest);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
try {
|
|
549
|
+
fs3.renameSync(versionDir(rootDir, source), dest);
|
|
550
|
+
} catch (err) {
|
|
551
|
+
log(`[single-copy] migration rename of versions/${source} failed (${errMsg(err)})`);
|
|
552
|
+
return { migrated: false, version: null, reason: errMsg(err) };
|
|
553
|
+
}
|
|
554
|
+
rmrf(versionsDir(rootDir));
|
|
555
|
+
rmrf(stateFilePath(rootDir));
|
|
556
|
+
const dataDir = path3.dirname(rootDir);
|
|
557
|
+
for (const marker of LEGACY_FRAMEWORK_MARKERS) rmrf(path3.join(dataDir, marker));
|
|
558
|
+
log(`[single-copy] migrated legacy versions/${source} \u2192 current`);
|
|
559
|
+
return { migrated: true, version: source, reason: "adopted legacy version" };
|
|
560
|
+
}
|
|
561
|
+
function sweepTransientRootDirs(rootDir, now, staleMs) {
|
|
562
|
+
let entries;
|
|
563
|
+
try {
|
|
564
|
+
entries = fs3.readdirSync(rootDir);
|
|
565
|
+
} catch {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
for (const entry of entries) {
|
|
569
|
+
if (!entry.startsWith(".staging-") && !entry.startsWith(".trash-")) continue;
|
|
570
|
+
const full = path3.join(rootDir, entry);
|
|
571
|
+
try {
|
|
572
|
+
if (now - fs3.statSync(full).mtimeMs < staleMs) continue;
|
|
573
|
+
} catch {
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
rmrf(full);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function planBoot(currentValid) {
|
|
580
|
+
if (currentValid) {
|
|
581
|
+
return { kind: "current", reason: "single-copy closure present and valid" };
|
|
582
|
+
}
|
|
583
|
+
return { kind: "baked", reason: "no valid single-copy closure \u2014 booting the baked seed" };
|
|
584
|
+
}
|
|
585
|
+
var fs4 = __toESM2(require("fs"));
|
|
586
|
+
var path4 = __toESM2(require("path"));
|
|
434
587
|
function detectWorkspaceRoot(fromDir) {
|
|
435
|
-
let dir =
|
|
588
|
+
let dir = path4.resolve(fromDir);
|
|
436
589
|
for (; ; ) {
|
|
437
|
-
const pkgPath =
|
|
590
|
+
const pkgPath = path4.join(dir, "package.json");
|
|
438
591
|
try {
|
|
439
|
-
const raw = JSON.parse(
|
|
592
|
+
const raw = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
|
|
440
593
|
if (typeof raw === "object" && raw !== null && raw["workspaces"] !== void 0) {
|
|
441
594
|
return dir;
|
|
442
595
|
}
|
|
443
596
|
} catch {
|
|
444
597
|
}
|
|
445
|
-
const parent =
|
|
598
|
+
const parent = path4.dirname(dir);
|
|
446
599
|
if (parent === dir) return null;
|
|
447
600
|
dir = parent;
|
|
448
601
|
}
|
|
449
602
|
}
|
|
450
|
-
var
|
|
451
|
-
var
|
|
603
|
+
var fs5 = __toESM2(require("fs"));
|
|
604
|
+
var path5 = __toESM2(require("path"));
|
|
452
605
|
var import_node_url = require("url");
|
|
453
606
|
var HOST_EXTERNAL_SPECIFIERS = [
|
|
454
607
|
"@camstack/system",
|
|
@@ -476,7 +629,7 @@ var require_dist = __commonJS({
|
|
|
476
629
|
return;
|
|
477
630
|
}
|
|
478
631
|
const anchorURL = (0, import_node_url.pathToFileURL)(
|
|
479
|
-
|
|
632
|
+
path5.join(activeRootDir, "node_modules", "__camstack_starter_anchor__.js")
|
|
480
633
|
).href;
|
|
481
634
|
const hooks = {
|
|
482
635
|
resolve: (specifier, context, nextResolve) => {
|
|
@@ -491,23 +644,24 @@ var require_dist = __commonJS({
|
|
|
491
644
|
};
|
|
492
645
|
registerHooks(hooks);
|
|
493
646
|
}
|
|
494
|
-
function
|
|
647
|
+
function readClosureVersion2(closureDir, spec) {
|
|
495
648
|
try {
|
|
496
|
-
const
|
|
497
|
-
|
|
649
|
+
const raw = JSON.parse(
|
|
650
|
+
fs5.readFileSync(path5.join(rootPackageDir(closureDir, spec), "package.json"), "utf-8")
|
|
651
|
+
);
|
|
498
652
|
if (typeof raw === "object" && raw !== null) {
|
|
499
653
|
const version = raw["version"];
|
|
500
654
|
if (typeof version === "string") return version;
|
|
501
655
|
}
|
|
502
|
-
return null;
|
|
503
656
|
} catch {
|
|
504
|
-
return null;
|
|
505
657
|
}
|
|
658
|
+
return null;
|
|
506
659
|
}
|
|
507
660
|
function runNodeStarter2(options) {
|
|
508
661
|
const env = options.env ?? process.env;
|
|
509
662
|
const now = options.now ?? Date.now;
|
|
510
663
|
const registerResolver = options.registerResolver ?? registerActiveRootResolver;
|
|
664
|
+
const nodeMajor = options.nodeMajor ?? Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
|
|
511
665
|
const disabled = env[options.envNames.killSwitch] === "off";
|
|
512
666
|
const workspaceRoot = detectWorkspaceRoot(options.starterDir);
|
|
513
667
|
if (workspaceRoot !== null) {
|
|
@@ -525,40 +679,34 @@ var require_dist = __commonJS({
|
|
|
525
679
|
return;
|
|
526
680
|
}
|
|
527
681
|
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
|
-
}
|
|
682
|
+
const swap = applyPendingRootSwap(rootDir, options.spec, nodeMajor, now, (m) => console.log(m));
|
|
683
|
+
if (swap.applied) console.log(`[starter] applied staged update \u2192 ${swap.version ?? "?"}`);
|
|
684
|
+
else if (swap.reason !== "no pending swap")
|
|
685
|
+
console.warn(`[starter] staged update NOT applied: ${swap.reason}`);
|
|
686
|
+
const migration = migrateToSingleCopy(
|
|
687
|
+
rootDir,
|
|
688
|
+
options.spec,
|
|
689
|
+
nodeMajor,
|
|
690
|
+
now,
|
|
691
|
+
(m) => console.log(m)
|
|
692
|
+
);
|
|
693
|
+
if (migration.migrated)
|
|
694
|
+
console.log(`[starter] migrated legacy layout \u2192 single copy (${migration.version ?? "?"})`);
|
|
695
|
+
const activeRootDir = currentDir(rootDir);
|
|
696
|
+
const invalid = validateClosureDir(activeRootDir, nodeMajor, options.spec);
|
|
697
|
+
if (invalid !== null && fs5.existsSync(activeRootDir)) {
|
|
698
|
+
console.warn(`[starter] single copy invalid: ${invalid}`);
|
|
552
699
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
const
|
|
700
|
+
const plan = planBoot(invalid === null);
|
|
701
|
+
if (plan.kind === "current") {
|
|
702
|
+
const version = readClosureVersion2(activeRootDir, options.spec);
|
|
703
|
+
const entry = currentEntryPath(rootDir, options.spec);
|
|
556
704
|
console.log(
|
|
557
|
-
`[starter] booting ${options.spec.packageName}@${
|
|
705
|
+
`[starter] booting ${options.spec.packageName}@${version ?? "?"} from ${activeRootDir}`
|
|
558
706
|
);
|
|
559
707
|
env[options.envNames.bootMode] = "data-root";
|
|
560
708
|
env[options.envNames.activeRoot] = activeRootDir;
|
|
561
|
-
env[options.envNames.activeVersion] =
|
|
709
|
+
if (version !== null) env[options.envNames.activeVersion] = version;
|
|
562
710
|
registerResolver(activeRootDir);
|
|
563
711
|
options.loadEntry(entry);
|
|
564
712
|
return;
|
|
@@ -568,8 +716,8 @@ var require_dist = __commonJS({
|
|
|
568
716
|
options.loadEntry(options.seedEntry);
|
|
569
717
|
}
|
|
570
718
|
var import_node_child_process = require("child_process");
|
|
571
|
-
var
|
|
572
|
-
var
|
|
719
|
+
var fs6 = __toESM2(require("fs"));
|
|
720
|
+
var path6 = __toESM2(require("path"));
|
|
573
721
|
var import_node_util = require("util");
|
|
574
722
|
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
575
723
|
function buildNpmRegistryArgs(registry) {
|
|
@@ -579,9 +727,10 @@ var require_dist = __commonJS({
|
|
|
579
727
|
var NPM_VIEW_TIMEOUT_MS = 2e4;
|
|
580
728
|
var NPM_INSTALL_TIMEOUT_MS = 15 * 6e4;
|
|
581
729
|
var RESTART_REASON_PREFIX = "server-update";
|
|
730
|
+
var STALE_TRANSIENT_MS = 24 * 60 * 60 * 1e3;
|
|
582
731
|
function readPackageVersion(pkgJsonPath) {
|
|
583
732
|
try {
|
|
584
|
-
const raw = JSON.parse(
|
|
733
|
+
const raw = JSON.parse(fs6.readFileSync(pkgJsonPath, "utf-8"));
|
|
585
734
|
if (typeof raw === "object" && raw !== null) {
|
|
586
735
|
const version = raw["version"];
|
|
587
736
|
if (typeof version === "string") return version;
|
|
@@ -590,7 +739,7 @@ var require_dist = __commonJS({
|
|
|
590
739
|
}
|
|
591
740
|
return null;
|
|
592
741
|
}
|
|
593
|
-
var RootUpdateService = class
|
|
742
|
+
var RootUpdateService = class {
|
|
594
743
|
spec;
|
|
595
744
|
envNames;
|
|
596
745
|
logger;
|
|
@@ -608,7 +757,7 @@ var require_dist = __commonJS({
|
|
|
608
757
|
this.envNames = options.envNames;
|
|
609
758
|
this.logger = options.logger;
|
|
610
759
|
this.restartServerFn = options.restartServer;
|
|
611
|
-
this.dataDir =
|
|
760
|
+
this.dataDir = path6.resolve(options.dataDir);
|
|
612
761
|
this.execNpm = options.execNpm ?? (async (args, opts) => {
|
|
613
762
|
const { stdout } = await execFileAsync("npm", [...args], {
|
|
614
763
|
cwd: opts.cwd,
|
|
@@ -637,57 +786,37 @@ var require_dist = __commonJS({
|
|
|
637
786
|
seedVersion() {
|
|
638
787
|
const seedDir = this.env[this.envNames.seedDir];
|
|
639
788
|
if (seedDir === void 0 || seedDir.length === 0) return null;
|
|
640
|
-
return readPackageVersion(
|
|
789
|
+
return readPackageVersion(path6.join(seedDir, "package.json"));
|
|
641
790
|
}
|
|
642
791
|
rootDir() {
|
|
643
792
|
return serverRootDir(this.dataDir);
|
|
644
793
|
}
|
|
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) {
|
|
794
|
+
updateState() {
|
|
661
795
|
if (this.inFlight === "checking") return "checking";
|
|
662
796
|
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
|
-
}
|
|
797
|
+
if (readPendingRootSwap(this.rootDir()) !== null) return "pending-restart";
|
|
670
798
|
return "idle";
|
|
671
799
|
}
|
|
672
800
|
async getServerPackageStatus() {
|
|
673
|
-
const { state, corrupt } = this.readStateInfo();
|
|
674
801
|
const runningVersion = this.runningVersion();
|
|
675
802
|
const latestVersion = this.checkCache?.latestVersion ?? null;
|
|
803
|
+
const pending = readPendingRootSwap(this.rootDir());
|
|
676
804
|
const updateAvailable = latestVersion !== null && runningVersion !== null && compareSemver(latestVersion, runningVersion) > 0;
|
|
677
805
|
return {
|
|
678
806
|
packageName: this.spec.packageName,
|
|
679
807
|
runningVersion,
|
|
680
808
|
nodeRuntimeVersion: process.versions.node,
|
|
681
|
-
activeVersion: this.env[this.envNames.activeVersion] ??
|
|
682
|
-
|
|
809
|
+
activeVersion: this.env[this.envNames.activeVersion] ?? runningVersion,
|
|
810
|
+
// Single-copy model: no N-1 retention, no rollback record.
|
|
811
|
+
previousVersion: null,
|
|
683
812
|
seedVersion: this.seedVersion(),
|
|
684
813
|
latestVersion,
|
|
685
814
|
updateAvailable,
|
|
686
815
|
bootMode: this.resolveBootMode(),
|
|
687
|
-
updateState: this.updateState(
|
|
688
|
-
pendingVersion:
|
|
689
|
-
rolledBack:
|
|
690
|
-
stateFileCorrupt:
|
|
816
|
+
updateState: this.updateState(),
|
|
817
|
+
pendingVersion: pending?.version ?? null,
|
|
818
|
+
rolledBack: null,
|
|
819
|
+
stateFileCorrupt: false,
|
|
691
820
|
lastCheckedAtMs: this.checkCache?.checkedAtMs ?? null
|
|
692
821
|
};
|
|
693
822
|
}
|
|
@@ -764,13 +893,13 @@ var require_dist = __commonJS({
|
|
|
764
893
|
message: `Refused: another operation is in flight (${this.inFlight}).`
|
|
765
894
|
};
|
|
766
895
|
}
|
|
767
|
-
const
|
|
768
|
-
if (
|
|
896
|
+
const pendingBefore = readPendingRootSwap(this.rootDir());
|
|
897
|
+
if (pendingBefore !== null) {
|
|
769
898
|
return {
|
|
770
899
|
accepted: false,
|
|
771
900
|
targetVersion: input.version ?? null,
|
|
772
901
|
restarting: false,
|
|
773
|
-
message: `Refused: version ${
|
|
902
|
+
message: `Refused: version ${pendingBefore.version} is already staged and awaiting restart.`
|
|
774
903
|
};
|
|
775
904
|
}
|
|
776
905
|
let target = input.version ?? null;
|
|
@@ -797,7 +926,7 @@ var require_dist = __commonJS({
|
|
|
797
926
|
}
|
|
798
927
|
if (isDevChannelVersion(target)) {
|
|
799
928
|
const uploadsDir = devUploadVersionDir(this.rootDir(), target);
|
|
800
|
-
if (!
|
|
929
|
+
if (!fs6.existsSync(uploadsDir)) {
|
|
801
930
|
return {
|
|
802
931
|
accepted: false,
|
|
803
932
|
targetVersion: target,
|
|
@@ -807,8 +936,9 @@ var require_dist = __commonJS({
|
|
|
807
936
|
}
|
|
808
937
|
}
|
|
809
938
|
this.inFlight = "staging";
|
|
939
|
+
let stagingPath;
|
|
810
940
|
try {
|
|
811
|
-
await this.
|
|
941
|
+
stagingPath = await this.stageClosure(target);
|
|
812
942
|
} catch (err) {
|
|
813
943
|
const message = err instanceof Error ? err.message : String(err);
|
|
814
944
|
this.logger.error("root package update staging failed", {
|
|
@@ -823,45 +953,42 @@ var require_dist = __commonJS({
|
|
|
823
953
|
} finally {
|
|
824
954
|
this.inFlight = "idle";
|
|
825
955
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
fromVersion: state.currentVersion,
|
|
832
|
-
requestedAtMs: this.now(),
|
|
833
|
-
bootAttempts: 0
|
|
834
|
-
},
|
|
835
|
-
rolledBack: null
|
|
956
|
+
writePendingRootSwap(this.rootDir(), {
|
|
957
|
+
schemaVersion: 1,
|
|
958
|
+
version: target,
|
|
959
|
+
stagingPath,
|
|
960
|
+
requestedAtMs: this.now()
|
|
836
961
|
});
|
|
837
962
|
this.logger.info("root package update staged \u2014 restarting to apply", {
|
|
838
|
-
meta: { targetVersion: target, fromVersion:
|
|
963
|
+
meta: { targetVersion: target, fromVersion: runningVersion }
|
|
839
964
|
});
|
|
840
965
|
this.restartServerFn(`${RESTART_REASON_PREFIX}: ${this.spec.packageName}@${target}`);
|
|
841
966
|
return {
|
|
842
967
|
accepted: true,
|
|
843
968
|
targetVersion: target,
|
|
844
969
|
restarting: true,
|
|
845
|
-
message: `Staged ${this.spec.packageName}@${target} \u2014 restarting to apply (
|
|
970
|
+
message: `Staged ${this.spec.packageName}@${target} \u2014 restarting to apply (single automatic reboot).`
|
|
846
971
|
};
|
|
847
972
|
}
|
|
848
973
|
/**
|
|
849
|
-
* npm-install the target closure into a SAME-FS staging dir
|
|
850
|
-
*
|
|
974
|
+
* npm-install the target closure into a SAME-FS `.staging-*` dir, then
|
|
975
|
+
* validate it (entry + version + native prebuilds). Returns the staging dir
|
|
976
|
+
* path for the pending-root-swap marker; the STARTER swaps it into `current`
|
|
977
|
+
* on the next boot. Throws (cleaning up the staging dir) on any failure so
|
|
978
|
+
* `applyServerUpdate` reports a staging error and never arms a bad swap.
|
|
851
979
|
*/
|
|
852
|
-
async
|
|
980
|
+
async stageClosure(target) {
|
|
853
981
|
const rootDir = this.rootDir();
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
fs5.mkdirSync(stagingDir, { recursive: true });
|
|
982
|
+
fs6.mkdirSync(rootDir, { recursive: true });
|
|
983
|
+
const stagingDir = stagingDirPath(rootDir, target, process.pid, this.now());
|
|
984
|
+
fs6.mkdirSync(stagingDir, { recursive: true });
|
|
858
985
|
try {
|
|
859
986
|
const devDir = devUploadVersionDir(rootDir, target);
|
|
860
987
|
const registry = this.env["CAMSTACK_NPM_REGISTRY"];
|
|
861
988
|
const installArgs = ["install", "--omit=dev", "--no-audit", "--no-fund", "--loglevel=error"];
|
|
862
|
-
if (
|
|
863
|
-
|
|
864
|
-
|
|
989
|
+
if (fs6.existsSync(devDir)) {
|
|
990
|
+
fs6.writeFileSync(
|
|
991
|
+
path6.join(stagingDir, "package.json"),
|
|
865
992
|
JSON.stringify(this.buildDevUploadsPackageJson(devDir, target), null, 2),
|
|
866
993
|
"utf-8"
|
|
867
994
|
);
|
|
@@ -870,8 +997,8 @@ var require_dist = __commonJS({
|
|
|
870
997
|
timeoutMs: NPM_INSTALL_TIMEOUT_MS
|
|
871
998
|
});
|
|
872
999
|
} else {
|
|
873
|
-
|
|
874
|
-
|
|
1000
|
+
fs6.writeFileSync(
|
|
1001
|
+
path6.join(stagingDir, "package.json"),
|
|
875
1002
|
JSON.stringify({ name: "camstack-node-root", private: true }, null, 2),
|
|
876
1003
|
"utf-8"
|
|
877
1004
|
);
|
|
@@ -881,37 +1008,32 @@ var require_dist = __commonJS({
|
|
|
881
1008
|
);
|
|
882
1009
|
}
|
|
883
1010
|
const entry = rootEntryPath(stagingDir, this.spec);
|
|
884
|
-
if (!
|
|
1011
|
+
if (!fs6.existsSync(entry)) {
|
|
885
1012
|
throw new Error(`staged closure is missing the root entry (${entry})`);
|
|
886
1013
|
}
|
|
887
|
-
const
|
|
888
|
-
|
|
889
|
-
)
|
|
890
|
-
|
|
1014
|
+
const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
|
|
1015
|
+
const invalid = validateClosureDir(stagingDir, nodeMajor, this.spec, target);
|
|
1016
|
+
if (invalid !== null) {
|
|
1017
|
+
throw new Error(`staged closure invalid: ${invalid}`);
|
|
1018
|
+
}
|
|
1019
|
+
const missingNatives = findMissingNativePrebuilds(stagingDir);
|
|
1020
|
+
if (missingNatives.length > 0) {
|
|
891
1021
|
throw new Error(
|
|
892
|
-
`staged closure
|
|
1022
|
+
`staged closure missing native prebuilds: ${missingNatives.join(", ")} \u2014 refusing to arm a swap that would break the forked runners`
|
|
893
1023
|
);
|
|
894
1024
|
}
|
|
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);
|
|
1025
|
+
return stagingDir;
|
|
902
1026
|
} catch (err) {
|
|
903
|
-
await
|
|
1027
|
+
await fs6.promises.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
|
|
904
1028
|
throw err;
|
|
905
1029
|
}
|
|
906
1030
|
}
|
|
907
1031
|
/**
|
|
908
1032
|
* Synthetic staging package.json for the dev server-deploy channel:
|
|
909
1033
|
* `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.
|
|
1034
|
+
* tgz; `overrides` maps EVERY OTHER uploaded `@camstack/<name>` to its tgz.
|
|
912
1035
|
* 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).
|
|
1036
|
+
* version, a missing root package, or a listed-but-absent tgz all throw.
|
|
915
1037
|
*/
|
|
916
1038
|
buildDevUploadsPackageJson(devDir, target) {
|
|
917
1039
|
const manifest = readDevUploadManifest(devDir);
|
|
@@ -930,8 +1052,8 @@ var require_dist = __commonJS({
|
|
|
930
1052
|
);
|
|
931
1053
|
}
|
|
932
1054
|
const fileRef = (filename) => {
|
|
933
|
-
const abs =
|
|
934
|
-
if (!
|
|
1055
|
+
const abs = path6.join(devDir, filename);
|
|
1056
|
+
if (!fs6.existsSync(abs)) {
|
|
935
1057
|
throw new Error(`dev-uploads tarball listed in the manifest is missing: ${abs}`);
|
|
936
1058
|
}
|
|
937
1059
|
return `file:${abs}`;
|
|
@@ -948,77 +1070,28 @@ var require_dist = __commonJS({
|
|
|
948
1070
|
...Object.keys(overrides).length > 0 ? { overrides } : {}
|
|
949
1071
|
};
|
|
950
1072
|
}
|
|
951
|
-
// ── Rollback
|
|
1073
|
+
// ── Rollback (removed — single-copy has no N-1) ────────────────────────
|
|
1074
|
+
/**
|
|
1075
|
+
* Rollback is NOT supported in the single-copy model — there is no retained
|
|
1076
|
+
* N-1 copy to revert to (operator's accepted tradeoff). Recovery is manual:
|
|
1077
|
+
* re-apply a known-good version via {@link applyServerUpdate}, or reinstall
|
|
1078
|
+
* the image seed. Kept on the surface so the cap contract is unchanged.
|
|
1079
|
+
*/
|
|
952
1080
|
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
1081
|
return {
|
|
1009
|
-
accepted:
|
|
1010
|
-
targetVersion:
|
|
1011
|
-
restarting:
|
|
1012
|
-
message:
|
|
1082
|
+
accepted: false,
|
|
1083
|
+
targetVersion: null,
|
|
1084
|
+
restarting: false,
|
|
1085
|
+
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
1086
|
};
|
|
1014
1087
|
}
|
|
1015
1088
|
// ── Plain restart ─────────────────────────────────────────────────────
|
|
1016
1089
|
/**
|
|
1017
|
-
* Plain process restart — no version change.
|
|
1018
|
-
*
|
|
1019
|
-
*
|
|
1020
|
-
*
|
|
1021
|
-
*
|
|
1090
|
+
* Plain process restart — no version change. Refuse while a stage is in
|
|
1091
|
+
* flight (a concurrent npm install must not be interrupted) or while a
|
|
1092
|
+
* version is already staged awaiting restart (a naive bounce would boot the
|
|
1093
|
+
* OLD version and orphan the pending swap — the operator should apply
|
|
1094
|
+
* instead).
|
|
1022
1095
|
*/
|
|
1023
1096
|
restartNode() {
|
|
1024
1097
|
const runningVersion = this.runningVersion();
|
|
@@ -1030,13 +1103,12 @@ var require_dist = __commonJS({
|
|
|
1030
1103
|
message: `Refused: another operation is in flight (${this.inFlight}).`
|
|
1031
1104
|
};
|
|
1032
1105
|
}
|
|
1033
|
-
|
|
1034
|
-
if (state.pendingBoot !== null) {
|
|
1106
|
+
if (readPendingRootSwap(this.rootDir()) !== null) {
|
|
1035
1107
|
return {
|
|
1036
1108
|
accepted: false,
|
|
1037
1109
|
targetVersion: runningVersion,
|
|
1038
1110
|
restarting: false,
|
|
1039
|
-
message:
|
|
1111
|
+
message: "Refused: a version is staged and awaiting restart \u2014 apply it instead of a plain restart."
|
|
1040
1112
|
};
|
|
1041
1113
|
}
|
|
1042
1114
|
this.logger.info("plain node restart requested", { meta: { runningVersion } });
|
|
@@ -1050,103 +1122,35 @@ var require_dist = __commonJS({
|
|
|
1050
1122
|
}
|
|
1051
1123
|
// ── Boot health confirmation ──────────────────────────────────────────
|
|
1052
1124
|
/**
|
|
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.
|
|
1125
|
+
* Called by the post-boot path once the node is READY. In the single-copy
|
|
1126
|
+
* model there is no pending version to promote (the swap already happened in
|
|
1127
|
+
* the starter). This becomes a best-effort GC of orphaned transient dirs
|
|
1128
|
+
* (`.staging-*` / `.trash-*` left by a crash mid-swap) + the dev-uploads
|
|
1129
|
+
* sweep. Kept returning the legacy `{ promoted }` shape for the callers.
|
|
1059
1130
|
*/
|
|
1060
1131
|
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
|
-
);
|
|
1132
|
+
sweepTransientRootDirs(this.rootDir(), this.now(), STALE_TRANSIENT_MS);
|
|
1080
1133
|
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
|
-
}
|
|
1134
|
+
return { promoted: null };
|
|
1128
1135
|
}
|
|
1129
1136
|
/**
|
|
1130
1137
|
* Promote-time sweep of `server-root/dev-uploads/`: keep only the
|
|
1131
1138
|
* `keepCount` most recent entries (ordered by the self-describing
|
|
1132
1139
|
* `-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
1140
|
*/
|
|
1137
1141
|
sweepDevUploads(keepCount) {
|
|
1138
1142
|
const dir = devUploadsDir(this.rootDir());
|
|
1139
1143
|
let entries;
|
|
1140
1144
|
try {
|
|
1141
|
-
entries =
|
|
1145
|
+
entries = fs6.readdirSync(dir);
|
|
1142
1146
|
} catch {
|
|
1143
1147
|
return;
|
|
1144
1148
|
}
|
|
1145
|
-
const graveyard =
|
|
1149
|
+
const graveyard = path6.join(dir, ".sweeping");
|
|
1146
1150
|
try {
|
|
1147
|
-
for (const residue of
|
|
1151
|
+
for (const residue of fs6.readdirSync(graveyard)) {
|
|
1148
1152
|
try {
|
|
1149
|
-
|
|
1153
|
+
fs6.rmSync(path6.join(graveyard, residue), { recursive: true, force: true });
|
|
1150
1154
|
} catch {
|
|
1151
1155
|
}
|
|
1152
1156
|
}
|
|
@@ -1158,11 +1162,11 @@ var require_dist = __commonJS({
|
|
|
1158
1162
|
);
|
|
1159
1163
|
const doomed = byNewestFirst.slice(keepCount);
|
|
1160
1164
|
if (doomed.length === 0) return;
|
|
1161
|
-
|
|
1165
|
+
fs6.mkdirSync(graveyard, { recursive: true });
|
|
1162
1166
|
for (const entry of doomed) {
|
|
1163
|
-
const aside =
|
|
1167
|
+
const aside = path6.join(graveyard, `${entry}-${this.now()}`);
|
|
1164
1168
|
try {
|
|
1165
|
-
|
|
1169
|
+
fs6.renameSync(path6.join(dir, entry), aside);
|
|
1166
1170
|
} catch (err) {
|
|
1167
1171
|
this.logger.warn("failed to move dev-uploads entry aside for sweep", {
|
|
1168
1172
|
meta: { entry, error: err instanceof Error ? err.message : String(err) }
|
|
@@ -1170,14 +1174,12 @@ var require_dist = __commonJS({
|
|
|
1170
1174
|
continue;
|
|
1171
1175
|
}
|
|
1172
1176
|
try {
|
|
1173
|
-
|
|
1177
|
+
fs6.rmSync(aside, { recursive: true, force: true });
|
|
1174
1178
|
this.logger.debug("swept dev-uploads entry", { meta: { entry } });
|
|
1175
1179
|
} catch {
|
|
1176
1180
|
}
|
|
1177
1181
|
}
|
|
1178
1182
|
}
|
|
1179
|
-
/** Transient (.staging- / .evicted-) dirs younger than this are never pruned. */
|
|
1180
|
-
static STALE_TRANSIENT_MS = 24 * 60 * 60 * 1e3;
|
|
1181
1183
|
};
|
|
1182
1184
|
}
|
|
1183
1185
|
});
|