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