@camstack/server 1.1.71 → 1.1.72

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/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 applyPendingFrameworkSwap runs, so the swapped framework
48
- * code is what gets loaded. NEVER add a top-level static import of
49
- * @camstack/system here that would load the OLD framework before the swap.
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 applyPendingFrameworkSwap
275
- // so the swapped framework code is what gets loaded. Never import it at module top.
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
@@ -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
@@ -371,6 +371,38 @@ var require_dist = __commonJS({
371
371
  __name(validateVersionDir, "validateVersionDir");
372
372
  var fs3 = __toESM2(require("fs"));
373
373
  var path3 = __toESM2(require("path"));
374
+ function parse(version) {
375
+ const dashIdx = version.indexOf("-");
376
+ const base = dashIdx === -1 ? version : version.slice(0, dashIdx);
377
+ const prerelease = dashIdx === -1 ? null : version.slice(dashIdx + 1);
378
+ const nums = base.split(".").map((seg) => {
379
+ const n = Number.parseInt(seg, 10);
380
+ return Number.isNaN(n) ? 0 : n;
381
+ });
382
+ return {
383
+ nums,
384
+ prerelease
385
+ };
386
+ }
387
+ __name(parse, "parse");
388
+ function compareSemver(a, b) {
389
+ const pa = parse(a);
390
+ const pb = parse(b);
391
+ const len = Math.max(pa.nums.length, pb.nums.length);
392
+ for (let i = 0; i < len; i++) {
393
+ const na = pa.nums[i] ?? 0;
394
+ const nb = pb.nums[i] ?? 0;
395
+ if (na < nb) return -1;
396
+ if (na > nb) return 1;
397
+ }
398
+ if (pa.prerelease === null && pb.prerelease === null) return 0;
399
+ if (pa.prerelease === null) return 1;
400
+ if (pb.prerelease === null) return -1;
401
+ if (pa.prerelease < pb.prerelease) return -1;
402
+ if (pa.prerelease > pb.prerelease) return 1;
403
+ return 0;
404
+ }
405
+ __name(compareSemver, "compareSemver");
374
406
  var CURRENT_DIRNAME = "current";
375
407
  var PENDING_ROOT_SWAP_FILE = ".pending-root-swap.json";
376
408
  var LEGACY_FRAMEWORK_MARKERS = [
@@ -566,17 +598,43 @@ var require_dist = __commonJS({
566
598
  };
567
599
  }
568
600
  __name(applyPendingRootSwap, "applyPendingRootSwap");
601
+ function readClosureVersion(closureDir, spec) {
602
+ try {
603
+ const raw = JSON.parse(fs3.readFileSync(path3.join(rootPackageDir2(closureDir, spec), "package.json"), "utf-8"));
604
+ if (typeof raw === "object" && raw !== null) {
605
+ const version = raw["version"];
606
+ if (typeof version === "string") return version;
607
+ }
608
+ } catch {
609
+ }
610
+ return null;
611
+ }
612
+ __name(readClosureVersion, "readClosureVersion");
613
+ function isAdoptablePendingClosure(rootDir, version, nodeMajor, spec) {
614
+ const dir = versionDir(rootDir, version);
615
+ return validateClosureDir(dir, nodeMajor, spec, version) === null && findMissingNativePrebuilds(dir).length === 0;
616
+ }
617
+ __name(isAdoptablePendingClosure, "isAdoptablePendingClosure");
569
618
  function migrateToSingleCopy(rootDir, spec, nodeMajor, now, log) {
570
619
  const dest = currentDir(rootDir);
571
- if (fs3.existsSync(dest) && validateClosureDir(dest, nodeMajor, spec) === null) {
572
- return {
573
- migrated: false,
574
- version: null,
575
- reason: "current already present"
576
- };
620
+ const legacyState = readServerRootState(rootDir);
621
+ const pendingVersion = legacyState?.pendingBoot?.version ?? null;
622
+ const pendingAdoptable = pendingVersion !== null && isAdoptablePendingClosure(rootDir, pendingVersion, nodeMajor, spec);
623
+ const currentValid = fs3.existsSync(dest) && validateClosureDir(dest, nodeMajor, spec) === null;
624
+ if (currentValid) {
625
+ const currentVersion = readClosureVersion(dest, spec);
626
+ const pendingSupersedes = pendingAdoptable && pendingVersion !== null && (currentVersion === null || compareSemver(pendingVersion, currentVersion) > 0);
627
+ if (!pendingSupersedes) {
628
+ return {
629
+ migrated: false,
630
+ version: null,
631
+ reason: "current already present"
632
+ };
633
+ }
634
+ log(`[single-copy] in-flight pending ${pendingVersion} supersedes older current ${currentVersion ?? "?"}`);
577
635
  }
578
636
  const candidates = [];
579
- const legacyState = readServerRootState(rootDir);
637
+ if (pendingAdoptable && pendingVersion !== null) candidates.push(pendingVersion);
580
638
  if (legacyState !== null) {
581
639
  for (const v of [
582
640
  legacyState.currentVersion,
@@ -662,38 +720,6 @@ var require_dist = __commonJS({
662
720
  };
663
721
  }
664
722
  __name(planBoot, "planBoot");
665
- function parse(version) {
666
- const dashIdx = version.indexOf("-");
667
- const base = dashIdx === -1 ? version : version.slice(0, dashIdx);
668
- const prerelease = dashIdx === -1 ? null : version.slice(dashIdx + 1);
669
- const nums = base.split(".").map((seg) => {
670
- const n = Number.parseInt(seg, 10);
671
- return Number.isNaN(n) ? 0 : n;
672
- });
673
- return {
674
- nums,
675
- prerelease
676
- };
677
- }
678
- __name(parse, "parse");
679
- function compareSemver(a, b) {
680
- const pa = parse(a);
681
- const pb = parse(b);
682
- const len = Math.max(pa.nums.length, pb.nums.length);
683
- for (let i = 0; i < len; i++) {
684
- const na = pa.nums[i] ?? 0;
685
- const nb = pb.nums[i] ?? 0;
686
- if (na < nb) return -1;
687
- if (na > nb) return 1;
688
- }
689
- if (pa.prerelease === null && pb.prerelease === null) return 0;
690
- if (pa.prerelease === null) return 1;
691
- if (pb.prerelease === null) return -1;
692
- if (pa.prerelease < pb.prerelease) return -1;
693
- if (pa.prerelease > pb.prerelease) return 1;
694
- return 0;
695
- }
696
- __name(compareSemver, "compareSemver");
697
723
  var fs4 = __toESM2(require("fs"));
698
724
  var path4 = __toESM2(require("path"));
699
725
  function detectWorkspaceRoot(fromDir) {
@@ -756,7 +782,7 @@ var require_dist = __commonJS({
756
782
  registerHooks(hooks);
757
783
  }
758
784
  __name(registerActiveRootResolver, "registerActiveRootResolver");
759
- function readClosureVersion(closureDir, spec) {
785
+ function readClosureVersion2(closureDir, spec) {
760
786
  try {
761
787
  const raw = JSON.parse(fs5.readFileSync(path5.join(rootPackageDir2(closureDir, spec), "package.json"), "utf-8"));
762
788
  if (typeof raw === "object" && raw !== null) {
@@ -767,7 +793,7 @@ var require_dist = __commonJS({
767
793
  }
768
794
  return null;
769
795
  }
770
- __name(readClosureVersion, "readClosureVersion");
796
+ __name(readClosureVersion2, "readClosureVersion2");
771
797
  function runNodeStarter(options) {
772
798
  const env = options.env ?? process.env;
773
799
  const now = options.now ?? Date.now;
@@ -800,7 +826,7 @@ var require_dist = __commonJS({
800
826
  }
801
827
  const plan = planBoot(invalid === null);
802
828
  if (plan.kind === "current") {
803
- const version = readClosureVersion(activeRootDir, options.spec);
829
+ const version = readClosureVersion2(activeRootDir, options.spec);
804
830
  const entry = currentEntryPath(rootDir, options.spec);
805
831
  console.log(`[starter] booting ${options.spec.packageName}@${version ?? "?"} from ${activeRootDir}`);
806
832
  env[options.envNames.bootMode] = "data-root";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.1.71",
3
+ "version": "1.1.72",
4
4
  "private": false,
5
5
  "files": [
6
6
  "dist",
@@ -23,19 +23,19 @@
23
23
  "test:watch": "vitest"
24
24
  },
25
25
  "dependencies": {
26
- "@camstack/addon-admin-ui": "1.1.58",
26
+ "@camstack/addon-admin-ui": "1.1.59",
27
27
  "@camstack/addon-advanced-notifier": "1.1.30",
28
28
  "@camstack/addon-auth": "1.1.15",
29
29
  "@camstack/addon-decoder-nodeav": "1.1.18",
30
30
  "@camstack/addon-notifiers": "1.1.30",
31
- "@camstack/addon-pipeline": "1.1.64",
32
- "@camstack/addon-pipeline-orchestrator": "1.1.53",
33
- "@camstack/addon-post-analysis": "1.1.36",
31
+ "@camstack/addon-pipeline": "1.1.65",
32
+ "@camstack/addon-pipeline-orchestrator": "1.1.54",
33
+ "@camstack/addon-post-analysis": "1.1.37",
34
34
  "@camstack/sdk": "1.1.30",
35
35
  "@camstack/shm-ring": "1.0.29",
36
- "@camstack/system": "1.1.57",
37
- "@camstack/types": "1.1.51",
38
- "@camstack/ui-library": "1.1.42",
36
+ "@camstack/system": "1.1.58",
37
+ "@camstack/types": "1.1.52",
38
+ "@camstack/ui-library": "1.1.43",
39
39
  "@fastify/compress": "^9.0.0",
40
40
  "@fastify/cookie": "^11.0.2",
41
41
  "@fastify/multipart": "^10.0.0",
@@ -1,119 +0,0 @@
1
- "use strict";
2
- /**
3
- * Boot-time framework-swap job resume + health confirm.
4
- *
5
- * After a framework swap reboot, `post-boot.service.ts` calls this once the
6
- * hub is healthy. It:
7
- * 1. Reads `.framework-swap-confirm.json` (written by the launcher on apply).
8
- * 2. Marks the journal task `applied` → `done`, then finalises the job →
9
- * `completed`.
10
- * 3. Calls `confirmFrameworkSwapHealthy` to delete the confirm marker +
11
- * backup dirs (disarms the crash-loop rollback).
12
- *
13
- * Best-effort: a missing/corrupt journal is tolerated — `confirmFrameworkSwapHealthy`
14
- * is still called so the rollback is always disarmed when the hub boots healthy.
15
- */
16
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
17
- if (k2 === undefined) k2 = k;
18
- var desc = Object.getOwnPropertyDescriptor(m, k);
19
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
20
- desc = { enumerable: true, get: function() { return m[k]; } };
21
- }
22
- Object.defineProperty(o, k2, desc);
23
- }) : (function(o, m, k, k2) {
24
- if (k2 === undefined) k2 = k;
25
- o[k2] = m[k];
26
- }));
27
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
28
- Object.defineProperty(o, "default", { enumerable: true, value: v });
29
- }) : function(o, v) {
30
- o["default"] = v;
31
- });
32
- var __importStar = (this && this.__importStar) || (function () {
33
- var ownKeys = function(o) {
34
- ownKeys = Object.getOwnPropertyNames || function (o) {
35
- var ar = [];
36
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
37
- return ar;
38
- };
39
- return ownKeys(o);
40
- };
41
- return function (mod) {
42
- if (mod && mod.__esModule) return mod;
43
- var result = {};
44
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
45
- __setModuleDefault(result, mod);
46
- return result;
47
- };
48
- })();
49
- Object.defineProperty(exports, "__esModule", { value: true });
50
- exports.resumeFrameworkSwapJob = resumeFrameworkSwapJob;
51
- const fs = __importStar(require("node:fs"));
52
- const path = __importStar(require("node:path"));
53
- const types_1 = require("@camstack/types");
54
- const system_1 = require("@camstack/system");
55
- const launcher_framework_swap_js_1 = require("../launcher-framework-swap.js");
56
- const lifecycle_journal_path_js_1 = require("../lifecycle-journal-path.js");
57
- const SWAP_CONFIRM_FILE = '.framework-swap-confirm.json';
58
- /**
59
- * Resume a framework-swap journal job to `done`/`completed` and confirm the
60
- * hub is healthy (deletes the confirm marker + backups).
61
- *
62
- * @returns `{ resumed: false }` when no confirm marker exists.
63
- * `{ resumed: true, jobId }` when the marker was found and processed.
64
- * Never throws — errors are swallowed to avoid crashing the post-boot path.
65
- */
66
- async function resumeFrameworkSwapJob(dataDir) {
67
- try {
68
- const confirmMarker = readConfirmMarker(dataDir);
69
- if (confirmMarker === null) {
70
- return { resumed: false };
71
- }
72
- const { jobId, taskId } = confirmMarker;
73
- let journalPatched = false;
74
- try {
75
- const journal = new system_1.JobJournal((0, lifecycle_journal_path_js_1.lifecycleJobsDir)(dataDir));
76
- const job = journal.getJob(jobId);
77
- if (job !== null) {
78
- const task = job.tasks.find((t) => t.taskId === taskId);
79
- if (task !== undefined && task.phase === 'applied') {
80
- journal.patchTask(jobId, taskId, { phase: 'done', finishedAtMs: Date.now() });
81
- // Single-task framework job: if all tasks are now terminal and none
82
- // failed, mark the job completed (mirrors the engine's finalize logic).
83
- const updatedJob = journal.getJob(jobId);
84
- if (updatedJob !== null) {
85
- const allTerminal = updatedJob.tasks.every((t) => t.phase === 'done' || t.phase === 'failed' || t.phase === 'skipped');
86
- const anyFailed = updatedJob.tasks.some((t) => t.phase === 'failed');
87
- if (allTerminal && !anyFailed) {
88
- journal.setJobState(jobId, 'completed');
89
- }
90
- }
91
- journalPatched = true;
92
- }
93
- }
94
- }
95
- catch {
96
- // Journal is missing or corrupt — still clean up the confirm marker so
97
- // the rollback is disarmed on a healthy hub boot.
98
- }
99
- (0, launcher_framework_swap_js_1.confirmFrameworkSwapHealthy)(dataDir);
100
- return journalPatched ? { resumed: true, jobId } : { resumed: false };
101
- }
102
- catch {
103
- // Never crash the caller (post-boot service).
104
- return { resumed: false };
105
- }
106
- }
107
- /** Read and shape-check the confirm marker. Returns null on any error. */
108
- function readConfirmMarker(dataDir) {
109
- try {
110
- const raw = JSON.parse(fs.readFileSync(path.join(dataDir, SWAP_CONFIRM_FILE), 'utf-8'));
111
- const parsed = types_1.frameworkSwapConfirmSchema.safeParse(raw);
112
- if (!parsed.success)
113
- return null;
114
- return { jobId: parsed.data.jobId, taskId: parsed.data.taskId };
115
- }
116
- catch {
117
- return null;
118
- }
119
- }