@camstack/server 1.1.71 → 1.1.73
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/api/core/cap-providers.js +0 -11
- package/dist/api/core/lifecycle-job-runner.js +0 -2
- package/dist/api/trpc/cap-mount-helpers.js +21 -9
- package/dist/api/trpc/generated-cap-routers.js +0 -8
- package/dist/api/trpc/trpc.router.js +49 -2
- package/dist/boot/post-boot.service.js +4 -17
- package/dist/boot/reconcile-lifecycle-jobs.js +6 -5
- package/dist/core/addon/addon-package.service.js +17 -388
- package/dist/launcher.js +9 -19
- package/dist/manual-boot.js +0 -27
- package/dist/server-root/index.js +68 -42
- package/package.json +8 -8
- package/dist/boot/resume-framework-swap.js +0 -119
- package/dist/launcher-framework-swap.js +0 -408
- package/dist/request-framework-swap.js +0 -41
|
@@ -36,8 +36,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.AddonPackageService = exports.FRAMEWORK_PACKAGES = exports.SYSTEM_PACKAGE = void 0;
|
|
37
37
|
exports.isFrameworkPackage = isFrameworkPackage;
|
|
38
38
|
exports.extractTgzStripped = extractTgzStripped;
|
|
39
|
-
exports.sweepStaleFrameworkBackups = sweepStaleFrameworkBackups;
|
|
40
|
-
exports.swapInFrameworkPackage = swapInFrameworkPackage;
|
|
41
39
|
const fs = __importStar(require("node:fs"));
|
|
42
40
|
const path = __importStar(require("node:path"));
|
|
43
41
|
const os = __importStar(require("node:os"));
|
|
@@ -911,9 +909,9 @@ class AddonPackageService {
|
|
|
911
909
|
/**
|
|
912
910
|
* Download the npm tarball for `name@version` as a Buffer.
|
|
913
911
|
*
|
|
914
|
-
*
|
|
915
|
-
* lifecycle job engine (TarballFetcher signature). The
|
|
916
|
-
* forwarded to the underlying fetch calls so the AbortController
|
|
912
|
+
* Resolves the tarball URL from the registry packument, then fetches it,
|
|
913
|
+
* for use by the lifecycle job engine (TarballFetcher signature). The
|
|
914
|
+
* `signal` is forwarded to the underlying fetch calls so the AbortController
|
|
917
915
|
* timeout wired in LifecycleJobEngine fires correctly.
|
|
918
916
|
*/
|
|
919
917
|
async fetchAddonTarball(name, version, signal) {
|
|
@@ -1040,210 +1038,6 @@ class AddonPackageService {
|
|
|
1040
1038
|
}));
|
|
1041
1039
|
return rows;
|
|
1042
1040
|
}
|
|
1043
|
-
/**
|
|
1044
|
-
* Update one of the framework packages (manifest `camstack.system:
|
|
1045
|
-
* true`) and schedule a hub restart.
|
|
1046
|
-
*
|
|
1047
|
-
* Steps:
|
|
1048
|
-
* 1. Allow-list the package name (refuses anything not framework).
|
|
1049
|
-
* 2. Resolve `'latest'`/`undefined` to a concrete version via `npm view`.
|
|
1050
|
-
* 3. Run `npm install --prefix <appRoot> <name>@<version> --no-save`.
|
|
1051
|
-
* 4. Write a `.restart-pending` marker (kind: `framework-update`).
|
|
1052
|
-
* 5. Emit `system.restarting` event.
|
|
1053
|
-
* 6. `scheduleSelfRestart({ delayMs: 500 })` — gives the cap method
|
|
1054
|
-
* time to return before the WS drops.
|
|
1055
|
-
*
|
|
1056
|
-
* Returns BEFORE the exit fires so the admin UI receives `restartingAt`
|
|
1057
|
-
* and can pivot to the reconnect overlay.
|
|
1058
|
-
*/
|
|
1059
|
-
/**
|
|
1060
|
-
* Install the framework by SWAPPING each package's own directory, exactly
|
|
1061
|
-
* like every addon is installed (download the single-package tarball →
|
|
1062
|
-
* extract → atomic dir swap). We NEVER run `npm install --prefix <appRoot>`.
|
|
1063
|
-
*
|
|
1064
|
-
* Why: `npm install --prefix /data` manages the WHOLE node_modules tree — it
|
|
1065
|
-
* prunes every package not in @camstack/system's closure (the addons +
|
|
1066
|
-
* ui-library), deletes the stray manifest.json, and (via temp-install + merge
|
|
1067
|
-
* variants) can clobber working native bindings. A per-package tarball swap
|
|
1068
|
-
* touches only that package's folder; nothing else can be pruned, deleted, or
|
|
1069
|
-
* corrupted.
|
|
1070
|
-
*
|
|
1071
|
-
* Swaps the @camstack framework packages versioned in lockstep with system:
|
|
1072
|
-
* the pure-JS libs (types, sdk) AND @camstack/shm-ring. shm-ring is native
|
|
1073
|
-
* but SAFE to swap because it ships its compiled `.node` prebuilds INSIDE the
|
|
1074
|
-
* npm tarball (prebuildify + node-gyp-build resolves the matching one at
|
|
1075
|
-
* require-time) — a bare extract is sufficient, no build or download.
|
|
1076
|
-
*
|
|
1077
|
-
* It does NOT swap download/compile-on-install natives such as `better-sqlite3`
|
|
1078
|
-
* (prebuild-install fetches the binary from GitHub releases at install time —
|
|
1079
|
-
* a bare extract would leave it without a binding). Those are third-party,
|
|
1080
|
-
* hoisted, change only across majors, and are updated by an image redeploy.
|
|
1081
|
-
* System's own deps stay exactly where they are, bindings intact.
|
|
1082
|
-
*/
|
|
1083
|
-
async installFrameworkPackages(packageName, toVersion, appRoot) {
|
|
1084
|
-
const registry = process.env['CAMSTACK_NPM_REGISTRY'];
|
|
1085
|
-
// @camstack/shm-ring is native but ships prebuilds in its tarball → safe.
|
|
1086
|
-
const lockstepDeps = ['@camstack/types', '@camstack/sdk', '@camstack/shm-ring'];
|
|
1087
|
-
const swapTargets = [packageName, ...lockstepDeps];
|
|
1088
|
-
// Self-heal: a previous run killed mid-swap (e.g. process restart while a
|
|
1089
|
-
// slow download was in flight) can leave a `<pkg>.fw-bak` backup behind.
|
|
1090
|
-
// It's inert (not a valid package the loader picks up) but sweep it so the
|
|
1091
|
-
// tree stays clean.
|
|
1092
|
-
sweepStaleFrameworkBackups(appRoot);
|
|
1093
|
-
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'camstack-fw-'));
|
|
1094
|
-
try {
|
|
1095
|
-
for (const pkg of swapTargets) {
|
|
1096
|
-
const isPrimary = pkg === packageName;
|
|
1097
|
-
try {
|
|
1098
|
-
const tgzPath = await packTarball(pkg, toVersion, tmpRoot, registry);
|
|
1099
|
-
const extractDir = fs.mkdtempSync(path.join(tmpRoot, 'x-'));
|
|
1100
|
-
await execFileAsync('tar', ['-xzf', tgzPath, '-C', extractDir], { timeout: 60_000 });
|
|
1101
|
-
const result = swapInFrameworkPackage(path.join(extractDir, 'package'), appRoot);
|
|
1102
|
-
this.logger.info('updateFrameworkPackage: package swapped', {
|
|
1103
|
-
meta: { name: result.name, version: result.version },
|
|
1104
|
-
});
|
|
1105
|
-
}
|
|
1106
|
-
catch (err) {
|
|
1107
|
-
if (isPrimary)
|
|
1108
|
-
throw err;
|
|
1109
|
-
// A lockstep dep may not be published at this exact version (or the
|
|
1110
|
-
// download flaked) — keep the existing copy rather than fail the whole
|
|
1111
|
-
// framework update. System is the one that MUST succeed.
|
|
1112
|
-
this.logger.warn(`updateFrameworkPackage: lockstep dep ${pkg}@${toVersion} not swapped`, {
|
|
1113
|
-
meta: { error: (0, types_1.errMsg)(err) },
|
|
1114
|
-
});
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
finally {
|
|
1119
|
-
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
/**
|
|
1123
|
-
* Resolve a version specifier (`'latest'`, semver tag, exact) for a
|
|
1124
|
-
* framework package to a concrete version via `npm view`.
|
|
1125
|
-
* Delegates to the module-level `resolveNpmVersion` helper.
|
|
1126
|
-
* Exposed as a public method so the lifecycle job engine wiring in
|
|
1127
|
-
* cap-providers can pass it as `resolveVersion` without duplicating the
|
|
1128
|
-
* npm-view/manifest logic.
|
|
1129
|
-
*/
|
|
1130
|
-
async resolveFrameworkVersion(packageName, versionSpec) {
|
|
1131
|
-
return resolveNpmVersion(packageName, versionSpec, process.env['CAMSTACK_NPM_REGISTRY']);
|
|
1132
|
-
}
|
|
1133
|
-
/**
|
|
1134
|
-
* Return the currently-installed version of a framework package, or `null`
|
|
1135
|
-
* when it cannot be determined (not installed / no manifest).
|
|
1136
|
-
* Delegates to the module-level `readResolvedPackageManifest` helper.
|
|
1137
|
-
* Exposed as a public method for use by the lifecycle job engine wiring.
|
|
1138
|
-
*/
|
|
1139
|
-
currentFrameworkVersionOf(packageName) {
|
|
1140
|
-
const manifest = readResolvedPackageManifest(packageName);
|
|
1141
|
-
if (manifest !== null && typeof manifest['version'] === 'string') {
|
|
1142
|
-
return manifest['version'];
|
|
1143
|
-
}
|
|
1144
|
-
return null;
|
|
1145
|
-
}
|
|
1146
|
-
/**
|
|
1147
|
-
* Update a framework package via the staged launcher-swap engine.
|
|
1148
|
-
*
|
|
1149
|
-
* New path (when `runner` is provided):
|
|
1150
|
-
* 1. Resolve toVersion via `npm view`.
|
|
1151
|
-
* 2. Delegate to `runner.startJob` with a `target:'framework'` task.
|
|
1152
|
-
* The engine calls `stageFramework` → downloads+stages the 4 lockstep
|
|
1153
|
-
* packages → calls the wired `requestFrameworkSwap`
|
|
1154
|
-
* (`requestFrameworkSwapAndRestart`), which writes the
|
|
1155
|
-
* `.pending-framework-swap.json` marker AND schedules the self-restart.
|
|
1156
|
-
* The hub exits; the launcher applies the swap on the next boot. The
|
|
1157
|
-
* restart is owned by that seam, so this method does NOT schedule one.
|
|
1158
|
-
* 3. Return `{ packageName, fromVersion, toVersion, restartingAt }`.
|
|
1159
|
-
* `restartingAt = 0` only on the legacy in-hub path when `deferRestart`
|
|
1160
|
-
* is set (engine path always restarts once the framework is staged).
|
|
1161
|
-
*
|
|
1162
|
-
* Legacy path (when no `runner`): keeps the original in-hub live-swap for
|
|
1163
|
-
* backward compatibility with callers (e.g. tests) that have not wired the
|
|
1164
|
-
* engine deps.
|
|
1165
|
-
*/
|
|
1166
|
-
async updateFrameworkPackage(input) {
|
|
1167
|
-
const { packageName } = input;
|
|
1168
|
-
if (packageName !== exports.SYSTEM_PACKAGE) {
|
|
1169
|
-
throw new Error(`updateFrameworkPackage: '${packageName}' is not a framework package. Allowed: ${exports.SYSTEM_PACKAGE}`);
|
|
1170
|
-
}
|
|
1171
|
-
const fromVersion = this.currentFrameworkVersionOf(packageName) ?? 'unknown';
|
|
1172
|
-
const requestedVersion = input.version ?? 'latest';
|
|
1173
|
-
const toVersion = await this.resolveFrameworkVersion(packageName, requestedVersion);
|
|
1174
|
-
// ── New staged-swap engine path ──────────────────────────────────
|
|
1175
|
-
if (input.runner !== undefined) {
|
|
1176
|
-
this.logger.info('updateFrameworkPackage: delegating to staged swap engine', {
|
|
1177
|
-
meta: { packageName, fromVersion, toVersion, deferRestart: input.deferRestart ?? false },
|
|
1178
|
-
});
|
|
1179
|
-
await input.runner.startJob({
|
|
1180
|
-
kind: 'update',
|
|
1181
|
-
targets: [{ name: packageName, version: toVersion, target: 'framework' }],
|
|
1182
|
-
createdBy: input.requestedBy ?? 'system',
|
|
1183
|
-
});
|
|
1184
|
-
// The engine's framework task already wrote the pending-swap marker AND
|
|
1185
|
-
// scheduled the self-restart via the wired `requestFrameworkSwap`
|
|
1186
|
-
// (`requestFrameworkSwapAndRestart`) — that is the single restart owner,
|
|
1187
|
-
// so we must NOT schedule a second one here. We only surface the
|
|
1188
|
-
// pre-restart toast event for the interactive single-update path.
|
|
1189
|
-
if (input.deferRestart !== true) {
|
|
1190
|
-
this.eventBusService.emit({
|
|
1191
|
-
id: (0, node_crypto_1.randomUUID)(),
|
|
1192
|
-
timestamp: new Date(),
|
|
1193
|
-
source: { type: 'core', id: 'addon-package-service' },
|
|
1194
|
-
category: types_1.EventCategory.SystemRestarting,
|
|
1195
|
-
data: {
|
|
1196
|
-
kind: 'framework-update',
|
|
1197
|
-
packageName,
|
|
1198
|
-
fromVersion,
|
|
1199
|
-
toVersion,
|
|
1200
|
-
requestedAt: Date.now(),
|
|
1201
|
-
},
|
|
1202
|
-
});
|
|
1203
|
-
}
|
|
1204
|
-
const restartingAt = input.deferRestart === true ? 0 : Date.now() + 500;
|
|
1205
|
-
return { packageName, fromVersion, toVersion, restartingAt };
|
|
1206
|
-
}
|
|
1207
|
-
// ── Legacy in-hub live-swap path (no runner injected) ───────────
|
|
1208
|
-
const appRoot = resolveFrameworkPackageAppRoot(packageName, this.logger);
|
|
1209
|
-
this.logger.info('updateFrameworkPackage: installing (legacy in-hub path)', {
|
|
1210
|
-
meta: { packageName, fromVersion, toVersion, appRoot },
|
|
1211
|
-
});
|
|
1212
|
-
await this.installFrameworkPackages(packageName, toVersion, appRoot);
|
|
1213
|
-
if (input.deferRestart === true) {
|
|
1214
|
-
this.logger.info(`updateFrameworkPackage(${packageName}@${toVersion}): install done, restart deferred`);
|
|
1215
|
-
// Sentinel: 0 signals "no restart scheduled" to the caller
|
|
1216
|
-
return { packageName, fromVersion, toVersion, restartingAt: 0 };
|
|
1217
|
-
}
|
|
1218
|
-
const restartingAt = Date.now();
|
|
1219
|
-
const markerPayload = {
|
|
1220
|
-
kind: 'framework-update',
|
|
1221
|
-
packageName,
|
|
1222
|
-
fromVersion,
|
|
1223
|
-
toVersion,
|
|
1224
|
-
requestedAt: restartingAt,
|
|
1225
|
-
...(input.requestedBy !== undefined ? { requestedBy: input.requestedBy } : {}),
|
|
1226
|
-
};
|
|
1227
|
-
try {
|
|
1228
|
-
(0, system_1.writePendingRestart)(this.resolveDataDir(), markerPayload);
|
|
1229
|
-
}
|
|
1230
|
-
catch (err) {
|
|
1231
|
-
// The npm install already completed — the restart will still
|
|
1232
|
-
// pick up the new version, just without the completion toast.
|
|
1233
|
-
this.logger.warn('Failed to write restart marker after framework update', {
|
|
1234
|
-
meta: { error: (0, types_1.errMsg)(err) },
|
|
1235
|
-
});
|
|
1236
|
-
}
|
|
1237
|
-
this.eventBusService.emit({
|
|
1238
|
-
id: (0, node_crypto_1.randomUUID)(),
|
|
1239
|
-
timestamp: new Date(),
|
|
1240
|
-
source: { type: 'core', id: 'addon-package-service' },
|
|
1241
|
-
category: types_1.EventCategory.SystemRestarting,
|
|
1242
|
-
data: markerPayload,
|
|
1243
|
-
});
|
|
1244
|
-
(0, system_1.scheduleSelfRestart)({ delayMs: 500 });
|
|
1245
|
-
return { packageName, fromVersion, toVersion, restartingAt };
|
|
1246
|
-
}
|
|
1247
1041
|
// =========================================================================
|
|
1248
1042
|
// Reload
|
|
1249
1043
|
// =========================================================================
|
|
@@ -1333,10 +1127,12 @@ class AddonPackageService {
|
|
|
1333
1127
|
}
|
|
1334
1128
|
/**
|
|
1335
1129
|
* Run auto-update: check each installed package against its configured
|
|
1336
|
-
* channel, then route ALL packages that have a newer version through ONE
|
|
1337
|
-
* durable engine bulk job (F3 Task 5)
|
|
1338
|
-
*
|
|
1339
|
-
*
|
|
1130
|
+
* channel, then route ALL addon packages that have a newer version through ONE
|
|
1131
|
+
* durable engine bulk job (F3 Task 5); auto-updates survive a reboot via the
|
|
1132
|
+
* boot reconcile, exactly like a manual "Update all". The framework
|
|
1133
|
+
* (`@camstack/system`) is NOT applied by the engine — it ships via
|
|
1134
|
+
* `applyServerUpdate` (single-copy collapse), so a swept framework target is
|
|
1135
|
+
* skipped by the engine.
|
|
1340
1136
|
*
|
|
1341
1137
|
* Candidate selection (channel resolution + npm metadata fetch + "is there a
|
|
1342
1138
|
* newer version?" comparison) is unchanged from the previous per-item loop;
|
|
@@ -1394,9 +1190,10 @@ class AddonPackageService {
|
|
|
1394
1190
|
// Execute as ONE durable engine bulk job. The runner singleton is resolved
|
|
1395
1191
|
// lazily via a dynamic import (the singleton module transitively imports
|
|
1396
1192
|
// SYSTEM_PACKAGE back from this module) to avoid an init-order cycle. The
|
|
1397
|
-
// singleton is initialized at boot, before the scheduler timer fires.
|
|
1398
|
-
// framework target, if present, is
|
|
1399
|
-
// failure is logged, not thrown out
|
|
1193
|
+
// singleton is initialized at boot, before the scheduler timer fires. A
|
|
1194
|
+
// framework target, if present, is skipped by the engine (framework updates
|
|
1195
|
+
// ship via applyServerUpdate). Wrap so a failure is logged, not thrown out
|
|
1196
|
+
// of the timer callback.
|
|
1400
1197
|
try {
|
|
1401
1198
|
const { getLifecycleRunner } = await Promise.resolve().then(() => __importStar(require('../lifecycle/lifecycle-runner.singleton.js')));
|
|
1402
1199
|
const runner = getLifecycleRunner();
|
|
@@ -1754,7 +1551,7 @@ async function extractTgzStripped(tgz, destDir) {
|
|
|
1754
1551
|
}
|
|
1755
1552
|
}
|
|
1756
1553
|
// ---------------------------------------------------------------------------
|
|
1757
|
-
//
|
|
1554
|
+
// npm tarball / version helpers
|
|
1758
1555
|
// ---------------------------------------------------------------------------
|
|
1759
1556
|
/**
|
|
1760
1557
|
* Build the npm CLI args that pin every relevant registry to
|
|
@@ -1763,179 +1560,16 @@ async function extractTgzStripped(tgz, destDir) {
|
|
|
1763
1560
|
* user-home `.npmrc` files commonly declare
|
|
1764
1561
|
* `@camstack:registry=https://registry.npmjs.org/`, and that scoped
|
|
1765
1562
|
* entry takes precedence over the plain `--registry` CLI flag for
|
|
1766
|
-
* `@camstack/*` lookups
|
|
1767
|
-
* traverses.
|
|
1563
|
+
* `@camstack/*` lookups.
|
|
1768
1564
|
*
|
|
1769
|
-
* Without this,
|
|
1770
|
-
* `
|
|
1771
|
-
* their own private npm proxy via `@camstack:registry` would have
|
|
1772
|
-
* `updateFrameworkPackage` silently route around it.
|
|
1565
|
+
* Without this, an operator running their own private npm proxy via
|
|
1566
|
+
* `@camstack:registry` would have package downloads silently route around it.
|
|
1773
1567
|
*/
|
|
1774
1568
|
function buildNpmRegistryArgs(registry) {
|
|
1775
1569
|
if (registry === undefined || registry.length === 0)
|
|
1776
1570
|
return [];
|
|
1777
1571
|
return ['--registry', registry, `--@camstack:registry=${registry}`];
|
|
1778
1572
|
}
|
|
1779
|
-
/**
|
|
1780
|
-
* Remove any leftover `<pkg>.fw-bak` backup dirs under `appRoot/node_modules/
|
|
1781
|
-
* @camstack` — debris from a framework swap that was interrupted before its
|
|
1782
|
-
* own cleanup ran. Best-effort; returns the swept names.
|
|
1783
|
-
*/
|
|
1784
|
-
function sweepStaleFrameworkBackups(appRoot) {
|
|
1785
|
-
const scopeDir = path.join(appRoot, 'node_modules', '@camstack');
|
|
1786
|
-
if (!fs.existsSync(scopeDir))
|
|
1787
|
-
return [];
|
|
1788
|
-
const swept = [];
|
|
1789
|
-
for (const entry of fs.readdirSync(scopeDir, { withFileTypes: true })) {
|
|
1790
|
-
if (!entry.isDirectory() || !entry.name.endsWith('.fw-bak'))
|
|
1791
|
-
continue;
|
|
1792
|
-
try {
|
|
1793
|
-
fs.rmSync(path.join(scopeDir, entry.name), { recursive: true, force: true });
|
|
1794
|
-
swept.push(entry.name);
|
|
1795
|
-
}
|
|
1796
|
-
catch {
|
|
1797
|
-
// ignore — inert leftover, next sweep retries
|
|
1798
|
-
}
|
|
1799
|
-
}
|
|
1800
|
-
return swept;
|
|
1801
|
-
}
|
|
1802
|
-
/**
|
|
1803
|
-
* Download the `.tgz` for an EXACT `pkg@version` into `destRoot`, returning its
|
|
1804
|
-
* path. Prefers a direct HTTPS GET (registry metadata → tarball URL) because
|
|
1805
|
-
* `fetch` + `AbortSignal.timeout` aborts RELIABLY on a slow/flaky network — an
|
|
1806
|
-
* `npm pack` child can stall for minutes past its `execFile` timeout (npm holds
|
|
1807
|
-
* its stdio pipes open, so the awaited promise never settles), which would wedge
|
|
1808
|
-
* the whole framework update. Falls back to `npm pack` (SIGKILL on timeout) only
|
|
1809
|
-
* when the HTTPS path fails (private registry quirks, auth, etc.).
|
|
1810
|
-
*/
|
|
1811
|
-
async function packTarball(pkg, version, destRoot, registry) {
|
|
1812
|
-
const dir = fs.mkdtempSync(path.join(destRoot, 'dl-'));
|
|
1813
|
-
try {
|
|
1814
|
-
return await httpsDownloadTarball(pkg, version, dir, registry);
|
|
1815
|
-
}
|
|
1816
|
-
catch (httpErr) {
|
|
1817
|
-
const args = [
|
|
1818
|
-
'pack',
|
|
1819
|
-
`${pkg}@${version}`,
|
|
1820
|
-
'--pack-destination',
|
|
1821
|
-
dir,
|
|
1822
|
-
...buildNpmRegistryArgs(registry),
|
|
1823
|
-
];
|
|
1824
|
-
// SIGKILL (not the default SIGTERM): npm can ignore SIGTERM while a
|
|
1825
|
-
// grandchild holds the pipes — SIGKILL guarantees the timeout fires.
|
|
1826
|
-
await execFileAsync('npm', args, { timeout: 60_000, killSignal: 'SIGKILL' });
|
|
1827
|
-
const tgz = fs.readdirSync(dir).find((f) => f.endsWith('.tgz'));
|
|
1828
|
-
if (tgz === undefined) {
|
|
1829
|
-
throw new Error(`download failed for ${pkg}@${version}: ${(0, types_1.errMsg)(httpErr)}`, {
|
|
1830
|
-
cause: httpErr,
|
|
1831
|
-
});
|
|
1832
|
-
}
|
|
1833
|
-
return path.join(dir, tgz);
|
|
1834
|
-
}
|
|
1835
|
-
}
|
|
1836
|
-
/**
|
|
1837
|
-
* Direct HTTPS download of an exact `pkg@version` tarball. Resolves the tarball
|
|
1838
|
-
* URL from the registry packument, then streams it to a file. Every network
|
|
1839
|
-
* call is bounded by an `AbortSignal.timeout` so a DNS/registry stall fails fast
|
|
1840
|
-
* instead of hanging the framework update.
|
|
1841
|
-
*/
|
|
1842
|
-
async function httpsDownloadTarball(pkg, version, dir, registry) {
|
|
1843
|
-
const reg = (registry ?? 'https://registry.npmjs.org').replace(/\/+$/, '');
|
|
1844
|
-
const metaUrl = `${reg}/${encodeURIComponent(pkg).replace(/^%40/, '@')}`;
|
|
1845
|
-
const metaRes = await fetch(metaUrl, { signal: AbortSignal.timeout(20_000) });
|
|
1846
|
-
if (!metaRes.ok)
|
|
1847
|
-
throw new Error(`registry GET ${metaUrl} → ${metaRes.status}`);
|
|
1848
|
-
const meta = (await metaRes.json());
|
|
1849
|
-
const tarballUrl = meta.versions?.[version]?.dist?.tarball;
|
|
1850
|
-
if (typeof tarballUrl !== 'string') {
|
|
1851
|
-
throw new Error(`no tarball url for ${pkg}@${version}`);
|
|
1852
|
-
}
|
|
1853
|
-
const tarRes = await fetch(tarballUrl, { signal: AbortSignal.timeout(120_000) });
|
|
1854
|
-
if (!tarRes.ok)
|
|
1855
|
-
throw new Error(`tarball GET ${tarballUrl} → ${tarRes.status}`);
|
|
1856
|
-
const buf = Buffer.from(await tarRes.arrayBuffer());
|
|
1857
|
-
const outPath = path.join(dir, `${pkg.replace('@', '').replace('/', '-')}-${version}.tgz`);
|
|
1858
|
-
fs.writeFileSync(outPath, buf);
|
|
1859
|
-
return outPath;
|
|
1860
|
-
}
|
|
1861
|
-
/**
|
|
1862
|
-
* Atomically replace one `@camstack/*` package directory in
|
|
1863
|
-
* `appRoot/node_modules` with the contents of an extracted npm package dir
|
|
1864
|
-
* (`stagedPackageDir` = the `package/` folder from a `.tgz`).
|
|
1865
|
-
*
|
|
1866
|
-
* This is the framework-update primitive: it touches ONLY the target package's
|
|
1867
|
-
* folder. The current copy is renamed to a sibling `.fw-bak` backup first and
|
|
1868
|
-
* restored if the copy fails, so a crash mid-swap can't leave a half-written
|
|
1869
|
-
* package. Refuses anything outside the `@camstack/` scope as a safety guard.
|
|
1870
|
-
* Returns the installed package name + version.
|
|
1871
|
-
*/
|
|
1872
|
-
function swapInFrameworkPackage(stagedPackageDir, appRoot) {
|
|
1873
|
-
const pkgJsonPath = path.join(stagedPackageDir, 'package.json');
|
|
1874
|
-
if (!fs.existsSync(pkgJsonPath)) {
|
|
1875
|
-
throw new Error(`swapInFrameworkPackage: no package.json in ${stagedPackageDir}`);
|
|
1876
|
-
}
|
|
1877
|
-
const parsed = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
1878
|
-
const pkg = parsed;
|
|
1879
|
-
if (typeof pkg.name !== 'string' || typeof pkg.version !== 'string') {
|
|
1880
|
-
throw new Error(`swapInFrameworkPackage: invalid package.json in ${stagedPackageDir}`);
|
|
1881
|
-
}
|
|
1882
|
-
if (!pkg.name.startsWith('@camstack/')) {
|
|
1883
|
-
throw new Error(`swapInFrameworkPackage: refusing non-@camstack package ${pkg.name}`);
|
|
1884
|
-
}
|
|
1885
|
-
const targetDir = path.join(appRoot, 'node_modules', pkg.name);
|
|
1886
|
-
const backupDir = `${targetDir}.fw-bak`;
|
|
1887
|
-
fs.rmSync(backupDir, { recursive: true, force: true });
|
|
1888
|
-
const hadExisting = fs.existsSync(targetDir);
|
|
1889
|
-
if (hadExisting)
|
|
1890
|
-
fs.renameSync(targetDir, backupDir);
|
|
1891
|
-
try {
|
|
1892
|
-
fs.mkdirSync(path.dirname(targetDir), { recursive: true });
|
|
1893
|
-
fs.cpSync(stagedPackageDir, targetDir, { recursive: true });
|
|
1894
|
-
}
|
|
1895
|
-
catch (err) {
|
|
1896
|
-
// Roll back to the previous copy so a failed swap never leaves the
|
|
1897
|
-
// package missing or half-written.
|
|
1898
|
-
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
1899
|
-
if (hadExisting)
|
|
1900
|
-
fs.renameSync(backupDir, targetDir);
|
|
1901
|
-
throw err;
|
|
1902
|
-
}
|
|
1903
|
-
fs.rmSync(backupDir, { recursive: true, force: true });
|
|
1904
|
-
return { name: pkg.name, version: pkg.version };
|
|
1905
|
-
}
|
|
1906
|
-
/**
|
|
1907
|
-
* Resolve the directory whose `node_modules/<pkg>/` holds the currently-
|
|
1908
|
-
* installed copy of a framework package. `npm install --prefix <appRoot>`
|
|
1909
|
-
* will then update that exact copy in place.
|
|
1910
|
-
*
|
|
1911
|
-
* Strategy: ask Node's resolver where it finds the package today, then walk
|
|
1912
|
-
* up to the `node_modules/`-parent. This matches whatever resolution path
|
|
1913
|
-
* the running hub actually uses (server-local node_modules in prod;
|
|
1914
|
-
* workspace-root in dev; bundled in Electron) without hard-coding either.
|
|
1915
|
-
*
|
|
1916
|
-
* Test knob: `CAMSTACK_FRAMEWORK_APP_ROOT_OVERRIDE` short-circuits the walk
|
|
1917
|
-
* and returns the env-supplied path. Used by the e2e suite to redirect the
|
|
1918
|
-
* `npm install --prefix` side-effects into an isolated temp dir instead of
|
|
1919
|
-
* the workspace's `server/backend/node_modules/`. Never set in production.
|
|
1920
|
-
*/
|
|
1921
|
-
function resolveFrameworkPackageAppRoot(packageName, logger) {
|
|
1922
|
-
const override = process.env['CAMSTACK_FRAMEWORK_APP_ROOT_OVERRIDE'];
|
|
1923
|
-
if (override !== undefined && override.length > 0) {
|
|
1924
|
-
return override;
|
|
1925
|
-
}
|
|
1926
|
-
const resolved = require.resolve(`${packageName}/package.json`);
|
|
1927
|
-
// …/<appRoot>/node_modules/<scope>/<name>/package.json
|
|
1928
|
-
// walk up: package.json → name → scope → node_modules → appRoot
|
|
1929
|
-
let dir = path.dirname(resolved);
|
|
1930
|
-
while (dir !== path.dirname(dir)) {
|
|
1931
|
-
if (path.basename(dir) === 'node_modules') {
|
|
1932
|
-
return path.dirname(dir);
|
|
1933
|
-
}
|
|
1934
|
-
dir = path.dirname(dir);
|
|
1935
|
-
}
|
|
1936
|
-
logger.warn(`Could not resolve appRoot for ${packageName}; falling back to process.cwd()`);
|
|
1937
|
-
return process.cwd();
|
|
1938
|
-
}
|
|
1939
1573
|
/**
|
|
1940
1574
|
* Read a framework package's `package.json`, resolved however the
|
|
1941
1575
|
* running hub actually loads it — workspace symlink in dev, a real
|
|
@@ -1946,11 +1580,6 @@ function resolveFrameworkPackageAppRoot(packageName, logger) {
|
|
|
1946
1580
|
* `@camstack/sdk`, `@camstack/ui-library`) make that throw — so we
|
|
1947
1581
|
* fall back to resolving the package's main entry and walking up to
|
|
1948
1582
|
* the first `package.json` whose `name` matches.
|
|
1949
|
-
*
|
|
1950
|
-
* This is deliberately independent of `resolveFrameworkPackageAppRoot`:
|
|
1951
|
-
* that walk only finds a real `node_modules`-parent, which doesn't
|
|
1952
|
-
* exist for workspace-symlinked packages in dev — the cause of the
|
|
1953
|
-
* `vunknown` version label in the System Packages UI.
|
|
1954
1583
|
*/
|
|
1955
1584
|
function readResolvedPackageManifest(packageName) {
|
|
1956
1585
|
try {
|
package/dist/launcher.js
CHANGED
|
@@ -44,15 +44,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
44
44
|
* 3. Dynamically import main.ts (which has static @camstack/system imports)
|
|
45
45
|
*
|
|
46
46
|
* Load-order invariant: @camstack/system is imported DYNAMICALLY inside
|
|
47
|
-
* launch() AFTER
|
|
48
|
-
*
|
|
49
|
-
*
|
|
47
|
+
* launch() AFTER the active framework dir is resolved (the single-copy
|
|
48
|
+
* collapse points CAMSTACK_FRAMEWORK_DIR at CAMSTACK_SERVER_ACTIVE_ROOT in
|
|
49
|
+
* data-root mode, or the baked seed otherwise). NEVER add a top-level static
|
|
50
|
+
* import of @camstack/system here — that would pin the resolver before the
|
|
51
|
+
* framework dir + NODE_PATH are set up. Framework/cap updates ship via the
|
|
52
|
+
* node-root `applyServerUpdate` flow, not an in-launcher package swap.
|
|
50
53
|
*/
|
|
51
54
|
const fs = __importStar(require("node:fs"));
|
|
52
55
|
const path = __importStar(require("node:path"));
|
|
53
56
|
const tar = __importStar(require("tar"));
|
|
54
57
|
const yaml = __importStar(require("js-yaml"));
|
|
55
|
-
const launcher_framework_swap_js_1 = require("./launcher-framework-swap.js");
|
|
56
58
|
const framework_nodepath_js_1 = require("./framework-nodepath.js");
|
|
57
59
|
/** Path of the manifest file embedded inside every archive. */
|
|
58
60
|
const ARCHIVE_MANIFEST_NAME = '.camstack-backup-manifest.json';
|
|
@@ -241,19 +243,6 @@ async function launch() {
|
|
|
241
243
|
// `CAMSTACK_ADDONS_DIR` lets the Docker/Electron images point at their
|
|
242
244
|
// node_modules path (e.g. /data/node_modules); dev keeps `<dataDir>/addons`.
|
|
243
245
|
const addonsDir = process.env['CAMSTACK_ADDONS_DIR'] ?? path.resolve(dataDir, 'addons');
|
|
244
|
-
// ── Framework swap ────────────────────────────────────────────────────────
|
|
245
|
-
// MUST run before @camstack/system is loaded so the NEW framework code is
|
|
246
|
-
// what gets required. Both helpers are zero-dep (no @camstack imports).
|
|
247
|
-
// Roll back an unconfirmed prior swap (crash-loop guard), then apply any
|
|
248
|
-
// freshly-staged swap.
|
|
249
|
-
const frameworkDir = process.env['CAMSTACK_FRAMEWORK_DIR'];
|
|
250
|
-
const rb = (0, launcher_framework_swap_js_1.rollbackUnconfirmedFrameworkSwap)(dataDir, frameworkDir);
|
|
251
|
-
if (rb.rolledBack)
|
|
252
|
-
console.log('[launcher] Rolled back an unconfirmed framework update');
|
|
253
|
-
const fsw = (0, launcher_framework_swap_js_1.applyPendingFrameworkSwap)(dataDir, frameworkDir);
|
|
254
|
-
if (fsw.applied)
|
|
255
|
-
console.log(`[launcher] Applied staged framework update (job ${fsw.jobId ?? '?'})`);
|
|
256
|
-
// ── End framework swap ────────────────────────────────────────────────────
|
|
257
246
|
// ── Single-copy framework dir (2026-07-18 collapse) ────────────────────────
|
|
258
247
|
// In data-root mode the ACTIVE single copy (set by the starter as
|
|
259
248
|
// CAMSTACK_SERVER_ACTIVE_ROOT) IS the framework — point the forked
|
|
@@ -271,8 +260,9 @@ async function launch() {
|
|
|
271
260
|
// installer + symlink step pick up — otherwise we'd install a stale
|
|
272
261
|
// set and overwrite it a step later.
|
|
273
262
|
await applyPendingRestore(dataDir);
|
|
274
|
-
// @camstack/system is imported DYNAMICALLY here — AFTER
|
|
275
|
-
//
|
|
263
|
+
// @camstack/system is imported DYNAMICALLY here — AFTER the active framework
|
|
264
|
+
// dir + NODE_PATH are resolved above — so the correct framework copy is what
|
|
265
|
+
// gets loaded. Never import it at module top.
|
|
276
266
|
const { AddonInstaller, bootstrapSchema, detectWorkspacePackagesDir } = await Promise.resolve().then(() => __importStar(require('@camstack/system')));
|
|
277
267
|
// Install source resolution:
|
|
278
268
|
// 1. CAMSTACK_BUNDLED_ADDONS_DIR — set by Electron-packaged builds
|
package/dist/manual-boot.js
CHANGED
|
@@ -47,10 +47,8 @@ exports.bootManual = bootManual;
|
|
|
47
47
|
const path = __importStar(require("node:path"));
|
|
48
48
|
const node_crypto_1 = require("node:crypto");
|
|
49
49
|
const fastify_1 = __importDefault(require("fastify"));
|
|
50
|
-
const system_1 = require("@camstack/system");
|
|
51
50
|
const types_1 = require("@camstack/types");
|
|
52
51
|
const lifecycle_runner_singleton_1 = require("./core/lifecycle/lifecycle-runner.singleton");
|
|
53
|
-
const request_framework_swap_1 = require("./request-framework-swap");
|
|
54
52
|
const config_service_1 = require("./core/config/config.service");
|
|
55
53
|
const logging_service_1 = require("./core/logging/logging.service");
|
|
56
54
|
const event_bus_service_1 = require("./core/events/event-bus.service");
|
|
@@ -175,31 +173,6 @@ async function bootManual(opts) {
|
|
|
175
173
|
});
|
|
176
174
|
}
|
|
177
175
|
},
|
|
178
|
-
// ── Framework swap deps ──────────────────────────────────────────
|
|
179
|
-
stageFramework: (task, signal) => (0, system_1.stageFrameworkLockstep)({
|
|
180
|
-
jobId: task.taskId,
|
|
181
|
-
toVersion: task.toVersion,
|
|
182
|
-
frameworkDir: process.env['CAMSTACK_FRAMEWORK_DIR'] ?? '',
|
|
183
|
-
stagingDir: path.join(lifecycleDataDir, 'lifecycle', 'staging'),
|
|
184
|
-
fetchTarball: (name, version, sig) => addonPackageService.fetchAddonTarball(name, version, sig),
|
|
185
|
-
extract: (tgz, destDir) => addonPackageService.extractTarball(tgz, destDir),
|
|
186
|
-
// Called only for the lockstep COMPANIONS (types/sdk/shm-ring) — the
|
|
187
|
-
// primary (@camstack/system) uses `task.toVersion` directly inside
|
|
188
|
-
// stageFrameworkLockstep. Companions are versioned independently on npm
|
|
189
|
-
// (system's package.json declares them as `*`), so resolve each to its
|
|
190
|
-
// own latest — pinning to `task.toVersion` fetches a non-existent
|
|
191
|
-
// `@camstack/types@<system-version>` tarball and fails the swap.
|
|
192
|
-
resolveVersion: (name) => addonPackageService.resolveFrameworkVersion(name, 'latest'),
|
|
193
|
-
currentVersionOf: (name) => addonPackageService.currentFrameworkVersionOf(name),
|
|
194
|
-
signal,
|
|
195
|
-
}),
|
|
196
|
-
requestFrameworkSwap: ({ jobId, taskId, packages }) => {
|
|
197
|
-
// Write the pending-swap marker AND schedule the reboot that applies it.
|
|
198
|
-
// The engine assumes the process exits after this returns; the restart is
|
|
199
|
-
// owned here (the single seam for every engine path: single update,
|
|
200
|
-
// "Update all" bulk, and the auto-update scheduler).
|
|
201
|
-
(0, request_framework_swap_1.requestFrameworkSwapAndRestart)(lifecycleDataDir, { jobId, taskId, packages });
|
|
202
|
-
},
|
|
203
176
|
});
|
|
204
177
|
const replEngineService = new repl_engine_service_1.ReplEngineService(addonRegistryService, eventBusService, loggingService);
|
|
205
178
|
// Runtime-updatable root package (phase 1: hub only). The service owns the
|