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