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