@camstack/server 1.0.5 → 1.0.6

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.
@@ -0,0 +1,408 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.writePendingFrameworkSwap = writePendingFrameworkSwap;
37
+ exports.applyPendingFrameworkSwap = applyPendingFrameworkSwap;
38
+ exports.rollbackUnconfirmedFrameworkSwap = rollbackUnconfirmedFrameworkSwap;
39
+ exports.confirmFrameworkSwapHealthy = confirmFrameworkSwapHealthy;
40
+ /**
41
+ * Launcher zero-dep framework swap/rollback helpers.
42
+ *
43
+ * These run before @camstack/system is imported — zero @camstack imports.
44
+ * Only node:fs + node:path. All writes are atomic (tmpfile+rename).
45
+ * All reads tolerate missing/corrupt files (no-op result).
46
+ */
47
+ const fs = __importStar(require("node:fs"));
48
+ const path = __importStar(require("node:path"));
49
+ const lifecycle_journal_path_js_1 = require("./lifecycle-journal-path.js");
50
+ // ---------------------------------------------------------------------------
51
+ // Marker file names
52
+ // ---------------------------------------------------------------------------
53
+ const PENDING_SWAP_MARKER = '.pending-framework-swap.json';
54
+ const SWAP_CONFIRM_MARKER = '.framework-swap-confirm.json';
55
+ // ---------------------------------------------------------------------------
56
+ // Helpers
57
+ // ---------------------------------------------------------------------------
58
+ /** Write a JSON file atomically via a tmp sibling + rename. */
59
+ function writeJsonAtomic(filePath, value) {
60
+ const tmp = `${filePath}.tmp`;
61
+ fs.writeFileSync(tmp, JSON.stringify(value), 'utf-8');
62
+ fs.renameSync(tmp, filePath);
63
+ }
64
+ /** Read + parse JSON, returning null on any error (missing / corrupt). */
65
+ function readJsonSafe(filePath) {
66
+ try {
67
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ }
73
+ /** Validate that a parsed value matches the PendingSwapMarker shape. */
74
+ function isPendingSwapMarker(value) {
75
+ if (typeof value !== 'object' || value === null)
76
+ return false;
77
+ const v = value;
78
+ if (typeof v['jobId'] !== 'string')
79
+ return false;
80
+ if (typeof v['taskId'] !== 'string')
81
+ return false;
82
+ if (!Array.isArray(v['packages']))
83
+ return false;
84
+ for (const pkg of v['packages']) {
85
+ if (typeof pkg !== 'object' || pkg === null)
86
+ return false;
87
+ const p = pkg;
88
+ if (typeof p['name'] !== 'string')
89
+ return false;
90
+ if (typeof p['stagedPath'] !== 'string')
91
+ return false;
92
+ if (typeof p['backupPath'] !== 'string')
93
+ return false;
94
+ }
95
+ if (typeof v['requestedAtMs'] !== 'number')
96
+ return false;
97
+ if (v['schemaVersion'] !== 1)
98
+ return false;
99
+ return true;
100
+ }
101
+ /** Validate that a parsed value matches the SwapConfirmMarker shape. */
102
+ function isSwapConfirmMarker(value) {
103
+ if (typeof value !== 'object' || value === null)
104
+ return false;
105
+ const v = value;
106
+ if (typeof v['jobId'] !== 'string')
107
+ return false;
108
+ if (typeof v['taskId'] !== 'string')
109
+ return false;
110
+ if (!Array.isArray(v['backups']))
111
+ return false;
112
+ for (const b of v['backups']) {
113
+ if (typeof b !== 'object' || b === null)
114
+ return false;
115
+ const entry = b;
116
+ if (typeof entry['name'] !== 'string')
117
+ return false;
118
+ if (typeof entry['backupPath'] !== 'string')
119
+ return false;
120
+ if (typeof entry['livePath'] !== 'string')
121
+ return false;
122
+ }
123
+ if (typeof v['appliedAtMs'] !== 'number')
124
+ return false;
125
+ if (typeof v['bootAttempts'] !== 'number')
126
+ return false;
127
+ if (v['schemaVersion'] !== 1)
128
+ return false;
129
+ return true;
130
+ }
131
+ /**
132
+ * Patch a specific task inside a journal job file atomically.
133
+ * Reads `<dataDir>/lifecycle/jobs/<jobId>.json`, updates tasks[taskId],
134
+ * and writes back atomically. Silently no-ops on any read/write error.
135
+ */
136
+ function patchJournalTask(dataDir, jobId, taskId, patch) {
137
+ const jobFile = path.join((0, lifecycle_journal_path_js_1.lifecycleJobsDir)(dataDir), `${jobId}.json`);
138
+ const raw = readJsonSafe(jobFile);
139
+ if (typeof raw !== 'object' || raw === null)
140
+ return;
141
+ const job = raw;
142
+ const tasks = job['tasks'];
143
+ if (!Array.isArray(tasks))
144
+ return;
145
+ const updatedTasks = tasks.map((task) => {
146
+ if (typeof task !== 'object' || task === null)
147
+ return task;
148
+ const t = task;
149
+ if (t['taskId'] !== taskId)
150
+ return task;
151
+ const updated = { ...t, phase: patch.phase };
152
+ if (patch.error !== undefined)
153
+ updated['error'] = patch.error;
154
+ if (patch.finishedAtMs !== undefined)
155
+ updated['finishedAtMs'] = patch.finishedAtMs;
156
+ return updated;
157
+ });
158
+ try {
159
+ writeJsonAtomic(jobFile, { ...job, tasks: updatedTasks });
160
+ }
161
+ catch {
162
+ // journal patch is best-effort — never crash the launcher
163
+ }
164
+ }
165
+ /** Copy a directory tree cross-device (fallback for EXDEV rename errors). */
166
+ function moveDirCrossDevice(src, dest) {
167
+ fs.cpSync(src, dest, { recursive: true });
168
+ fs.rmSync(src, { recursive: true, force: true });
169
+ }
170
+ /**
171
+ * Write the `.pending-framework-swap.json` marker atomically.
172
+ * Co-located with the reader (`applyPendingFrameworkSwap`) for symmetry.
173
+ * Zero @camstack deps — this file runs before @camstack/system is imported.
174
+ */
175
+ function writePendingFrameworkSwap(dataDir, marker) {
176
+ const markerPath = path.join(dataDir, PENDING_SWAP_MARKER);
177
+ writeJsonAtomic(markerPath, marker);
178
+ }
179
+ /**
180
+ * Apply a pending framework swap if a `.pending-framework-swap.json` marker
181
+ * exists in `dataDir`. Swaps each listed package atomically:
182
+ * 1. Backup: rename live → backupPath
183
+ * 2. Swap: rename staged → live (EXDEV fallback: cpSync + rmSync)
184
+ *
185
+ * On any mid-loop failure: restores already-swapped packages, deletes the
186
+ * marker, returns `{ applied: false }`.
187
+ * On success: writes `.framework-swap-confirm.json`, patches the journal
188
+ * task to `applied`, deletes the swap marker, returns `{ applied: true, jobId }`.
189
+ */
190
+ function applyPendingFrameworkSwap(dataDir, frameworkDir) {
191
+ if (!frameworkDir)
192
+ return { applied: false };
193
+ const markerPath = path.join(dataDir, PENDING_SWAP_MARKER);
194
+ if (!fs.existsSync(markerPath))
195
+ return { applied: false };
196
+ const raw = readJsonSafe(markerPath);
197
+ if (!isPendingSwapMarker(raw)) {
198
+ // malformed — delete to prevent blocking future updates
199
+ try {
200
+ fs.rmSync(markerPath, { force: true });
201
+ }
202
+ catch {
203
+ // ignore deletion error
204
+ }
205
+ return { applied: false };
206
+ }
207
+ const marker = raw;
208
+ const swapped = [];
209
+ for (const pkg of marker.packages) {
210
+ const livePath = path.join(frameworkDir, 'node_modules', pkg.name);
211
+ try {
212
+ // Step 1: backup live dir if it exists
213
+ if (fs.existsSync(livePath)) {
214
+ fs.mkdirSync(path.dirname(pkg.backupPath), { recursive: true });
215
+ fs.renameSync(livePath, pkg.backupPath);
216
+ }
217
+ // Step 2: move staged dir to live
218
+ try {
219
+ fs.mkdirSync(path.dirname(livePath), { recursive: true });
220
+ fs.renameSync(pkg.stagedPath, livePath);
221
+ }
222
+ catch (err) {
223
+ if (err.code === 'EXDEV') {
224
+ fs.mkdirSync(path.dirname(livePath), { recursive: true });
225
+ moveDirCrossDevice(pkg.stagedPath, livePath);
226
+ }
227
+ else {
228
+ throw err;
229
+ }
230
+ }
231
+ swapped.push({ livePath, backupPath: pkg.backupPath });
232
+ }
233
+ catch {
234
+ // Restore all already-swapped packages
235
+ for (const done of swapped) {
236
+ try {
237
+ if (fs.existsSync(done.livePath)) {
238
+ fs.rmSync(done.livePath, { recursive: true, force: true });
239
+ }
240
+ if (fs.existsSync(done.backupPath)) {
241
+ fs.mkdirSync(path.dirname(done.livePath), { recursive: true });
242
+ fs.renameSync(done.backupPath, done.livePath);
243
+ }
244
+ }
245
+ catch {
246
+ // best-effort restore
247
+ }
248
+ }
249
+ // Delete the pending marker so the next boot doesn't retry a broken swap
250
+ try {
251
+ fs.rmSync(markerPath, { force: true });
252
+ }
253
+ catch {
254
+ // ignore
255
+ }
256
+ return { applied: false };
257
+ }
258
+ }
259
+ // All packages swapped successfully — write confirm marker
260
+ const backups = marker.packages.map((pkg) => ({
261
+ name: pkg.name,
262
+ backupPath: pkg.backupPath,
263
+ livePath: path.join(frameworkDir, 'node_modules', pkg.name),
264
+ }));
265
+ const confirmMarker = {
266
+ jobId: marker.jobId,
267
+ taskId: marker.taskId,
268
+ backups,
269
+ appliedAtMs: Date.now(),
270
+ bootAttempts: 0,
271
+ schemaVersion: 1,
272
+ };
273
+ // Write confirm marker — if this fails, restore everything and fail the apply
274
+ try {
275
+ writeJsonAtomic(path.join(dataDir, SWAP_CONFIRM_MARKER), confirmMarker);
276
+ }
277
+ catch {
278
+ // Confirm marker write failed — restore all swapped packages and fail the apply
279
+ for (const done of swapped) {
280
+ try {
281
+ if (fs.existsSync(done.livePath)) {
282
+ fs.rmSync(done.livePath, { recursive: true, force: true });
283
+ }
284
+ if (fs.existsSync(done.backupPath)) {
285
+ fs.mkdirSync(path.dirname(done.livePath), { recursive: true });
286
+ fs.renameSync(done.backupPath, done.livePath);
287
+ }
288
+ }
289
+ catch {
290
+ // best-effort restore
291
+ }
292
+ }
293
+ // Delete the pending marker so the next boot doesn't retry
294
+ try {
295
+ fs.rmSync(markerPath, { force: true });
296
+ }
297
+ catch {
298
+ // ignore
299
+ }
300
+ return { applied: false };
301
+ }
302
+ // Patch journal task → applied (best-effort, doesn't fail the apply)
303
+ patchJournalTask(dataDir, marker.jobId, marker.taskId, { phase: 'applied' });
304
+ // Delete the swap marker
305
+ try {
306
+ fs.rmSync(markerPath, { force: true });
307
+ }
308
+ catch {
309
+ // ignore
310
+ }
311
+ return { applied: true, jobId: marker.jobId };
312
+ }
313
+ /**
314
+ * Check for an unconfirmed framework swap on boot.
315
+ *
316
+ * - `bootAttempts === 0`: this is the probation boot. Increment to 1,
317
+ * rewrite the marker, return `{ rolledBack: false }`.
318
+ * - `bootAttempts >= 1`: the previous probation boot crashed / didn't call
319
+ * `confirmFrameworkSwapHealthy`. Restore all backups, patch the journal
320
+ * task to `failed`, delete the confirm marker, return `{ rolledBack: true }`.
321
+ */
322
+ function rollbackUnconfirmedFrameworkSwap(dataDir, frameworkDir) {
323
+ if (!frameworkDir)
324
+ return { rolledBack: false };
325
+ const confirmPath = path.join(dataDir, SWAP_CONFIRM_MARKER);
326
+ if (!fs.existsSync(confirmPath))
327
+ return { rolledBack: false };
328
+ const raw = readJsonSafe(confirmPath);
329
+ if (!isSwapConfirmMarker(raw))
330
+ return { rolledBack: false };
331
+ const confirm = raw;
332
+ if (confirm.bootAttempts === 0) {
333
+ // Probation boot — increment attempts and let this boot proceed
334
+ const updated = { ...confirm, bootAttempts: 1 };
335
+ try {
336
+ writeJsonAtomic(confirmPath, updated);
337
+ }
338
+ catch {
339
+ // ignore write failure
340
+ }
341
+ return { rolledBack: false };
342
+ }
343
+ // bootAttempts >= 1: probation boot didn't become healthy — rollback
344
+ for (const backup of confirm.backups) {
345
+ try {
346
+ if (fs.existsSync(backup.livePath)) {
347
+ fs.rmSync(backup.livePath, { recursive: true, force: true });
348
+ }
349
+ if (fs.existsSync(backup.backupPath)) {
350
+ fs.mkdirSync(path.dirname(backup.livePath), { recursive: true });
351
+ fs.renameSync(backup.backupPath, backup.livePath);
352
+ }
353
+ }
354
+ catch {
355
+ // best-effort restore
356
+ }
357
+ }
358
+ patchJournalTask(dataDir, confirm.jobId, confirm.taskId, {
359
+ phase: 'failed',
360
+ error: 'framework update rolled back — boot did not become healthy',
361
+ finishedAtMs: Date.now(),
362
+ });
363
+ try {
364
+ fs.rmSync(confirmPath, { force: true });
365
+ }
366
+ catch {
367
+ // ignore
368
+ }
369
+ return { rolledBack: true };
370
+ }
371
+ /**
372
+ * Called by post-boot health check once the hub is confirmed healthy after
373
+ * a framework swap. Deletes all backups and the confirm marker so the old
374
+ * framework packages are cleaned up.
375
+ */
376
+ function confirmFrameworkSwapHealthy(dataDir) {
377
+ const confirmPath = path.join(dataDir, SWAP_CONFIRM_MARKER);
378
+ if (!fs.existsSync(confirmPath))
379
+ return;
380
+ const raw = readJsonSafe(confirmPath);
381
+ if (!isSwapConfirmMarker(raw)) {
382
+ // corrupt marker — still delete it
383
+ try {
384
+ fs.rmSync(confirmPath, { force: true });
385
+ }
386
+ catch {
387
+ // ignore
388
+ }
389
+ return;
390
+ }
391
+ const confirm = raw;
392
+ // Delete all backup dirs
393
+ for (const backup of confirm.backups) {
394
+ try {
395
+ fs.rmSync(backup.backupPath, { recursive: true, force: true });
396
+ }
397
+ catch {
398
+ // best-effort cleanup
399
+ }
400
+ }
401
+ // Delete the confirm marker
402
+ try {
403
+ fs.rmSync(confirmPath, { force: true });
404
+ }
405
+ catch {
406
+ // ignore
407
+ }
408
+ }
package/dist/launcher.js CHANGED
@@ -42,12 +42,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
42
42
  * 1. Run AddonInstaller.ensureRequiredPackages (zero core dependencies)
43
43
  * 2. Create symlinks so @camstack/system resolves from data/addons/
44
44
  * 3. Dynamically import main.ts (which has static @camstack/system imports)
45
+ *
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.
45
50
  */
46
51
  const fs = __importStar(require("node:fs"));
47
52
  const path = __importStar(require("node:path"));
48
53
  const tar = __importStar(require("tar"));
49
54
  const yaml = __importStar(require("js-yaml"));
50
- const system_1 = require("@camstack/system");
55
+ const launcher_framework_swap_js_1 = require("./launcher-framework-swap.js");
56
+ const framework_nodepath_js_1 = require("./framework-nodepath.js");
51
57
  /** Path of the manifest file embedded inside every archive. */
52
58
  const ARCHIVE_MANIFEST_NAME = '.camstack-backup-manifest.json';
53
59
  /** Resolve the data directory from env or default */
@@ -169,12 +175,15 @@ function readConfigYaml(dataDir) {
169
175
  * `symlink` is a dev-time convenience that points data/addons/<pkg>
170
176
  * directly at the workspace source dir — never set in shipped
171
177
  * containers or Electron bundles.
178
+ *
179
+ * `bootstrapSchema` is passed in (not imported at module top) so this
180
+ * helper can be called after the dynamic @camstack/system import in launch().
172
181
  */
173
- function readBootstrapInstallSource(dataDir) {
182
+ function readBootstrapInstallSource(dataDir, bootstrapSchema) {
174
183
  const raw = readConfigYaml(dataDir);
175
184
  if (raw === null)
176
185
  return undefined;
177
- const validation = system_1.bootstrapSchema.safeParse(raw);
186
+ const validation = bootstrapSchema.safeParse(raw);
178
187
  if (!validation.success)
179
188
  return undefined;
180
189
  return parseInstallSource(validation.data.bootstrap.installSource);
@@ -189,12 +198,34 @@ function readBootstrapInstallSource(dataDir) {
189
198
  * launcher runs before @camstack/system is imported and must stay free of
190
199
  * core dependencies. Just yaml.load + bootstrapSchema (kernel) for
191
200
  * shape validation.
201
+ *
202
+ * `bootstrapSchema` is passed in (not imported at module top) so this
203
+ * helper can be called after the dynamic @camstack/system import in launch().
204
+ */
205
+ /**
206
+ * Derive the bootstrap package list from THIS server's own package.json — the
207
+ * `@camstack/addon-*` deps plus `@camstack/system` (which ships the required
208
+ * infrastructure builtins). Read by absolute path (the package.json sits one
209
+ * level up from this launcher's dist dir), so it works in the slim image where
210
+ * the `@camstack/*` node_modules symlinks are stripped. Mirrors
211
+ * `AddonInstaller.deriveBootstrapList`, but without `require.resolve`.
192
212
  */
193
- function readBootstrapRequiredAddons(dataDir) {
213
+ function deriveBootstrapFromSelf() {
214
+ try {
215
+ const pkgPath = path.resolve(__dirname, '..', 'package.json');
216
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
217
+ return Object.keys(pkg.dependencies ?? {}).filter((name) => name.startsWith('@camstack/addon-') || name === '@camstack/system');
218
+ }
219
+ catch (err) {
220
+ console.warn(`[launcher] could not derive bootstrap list from package.json: ${err instanceof Error ? err.message : String(err)}`);
221
+ return [];
222
+ }
223
+ }
224
+ function readBootstrapRequiredAddons(dataDir, bootstrapSchema) {
194
225
  const raw = readConfigYaml(dataDir);
195
226
  if (raw === null)
196
227
  return null;
197
- const validation = system_1.bootstrapSchema.safeParse(raw);
228
+ const validation = bootstrapSchema.safeParse(raw);
198
229
  if (!validation.success) {
199
230
  console.warn(`[launcher] config.yaml failed bootstrapSchema validation: ${validation.error.message}`);
200
231
  return null;
@@ -210,10 +241,26 @@ async function launch() {
210
241
  // `CAMSTACK_ADDONS_DIR` lets the Docker/Electron images point at their
211
242
  // node_modules path (e.g. /data/node_modules); dev keeps `<dataDir>/addons`.
212
243
  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 ────────────────────────────────────────────────────
213
257
  // Restore must happen first so the snapshot's `addons/` is what the
214
258
  // installer + symlink step pick up — otherwise we'd install a stale
215
259
  // set and overwrite it a step later.
216
260
  await applyPendingRestore(dataDir);
261
+ // @camstack/system is imported DYNAMICALLY here — AFTER applyPendingFrameworkSwap —
262
+ // so the swapped framework code is what gets loaded. Never import it at module top.
263
+ const { AddonInstaller, bootstrapSchema, detectWorkspacePackagesDir } = await Promise.resolve().then(() => __importStar(require('@camstack/system')));
217
264
  // Install source resolution:
218
265
  // 1. CAMSTACK_BUNDLED_ADDONS_DIR — set by Electron-packaged builds
219
266
  // to <resourcesPath>/addons. Pre-built addons ship with the
@@ -230,7 +277,7 @@ async function launch() {
230
277
  // CAMSTACK_BUNDLED_ADDONS_DIR: Electron path, forces 'local'.
231
278
  // bootstrap.installSource (config.yaml): same set as env. Env wins.
232
279
  const rawEnvSource = process.env['CAMSTACK_INSTALL_SOURCE'];
233
- const yamlSource = readBootstrapInstallSource(dataDir);
280
+ const yamlSource = readBootstrapInstallSource(dataDir, bootstrapSchema);
234
281
  const explicitSource = parseInstallSource(rawEnvSource) ?? yamlSource;
235
282
  const bundledDir = process.env['CAMSTACK_BUNDLED_ADDONS_DIR'];
236
283
  let workspaceDir = null;
@@ -241,14 +288,14 @@ async function launch() {
241
288
  console.log(`[launcher] Using bundled addons from ${bundledDir}`);
242
289
  }
243
290
  else if (explicitSource === 'local' || explicitSource === 'symlink') {
244
- workspaceDir = (0, system_1.detectWorkspacePackagesDir)(__dirname);
291
+ workspaceDir = detectWorkspacePackagesDir(__dirname);
245
292
  if (workspaceDir === null) {
246
293
  console.warn(`[launcher] installSource=${explicitSource} requested but no workspace ` +
247
294
  `packages/ dir found from ${__dirname} — falling back to 'npm'`);
248
295
  resolvedSource = 'npm';
249
296
  }
250
297
  }
251
- const installer = new system_1.AddonInstaller({
298
+ const installer = new AddonInstaller({
252
299
  addonsDir,
253
300
  workspacePackagesDir: workspaceDir ?? undefined,
254
301
  installSource: resolvedSource,
@@ -257,15 +304,21 @@ async function launch() {
257
304
  // kernel's hard-coded REQUIRED_PACKAGES. The kernel never reaches into
258
305
  // SQL before this list is installed; everything here must be a known
259
306
  // package name that can be resolved from the workspace, the bundle, or
260
- // npm. Default (undefined) → fall back to the in-kernel constant.
261
- const bootstrapRequired = readBootstrapRequiredAddons(dataDir);
262
- if (bootstrapRequired !== null) {
263
- console.log(`[launcher] bootstrap.requiredAddons from config.yaml: ${bootstrapRequired.length} packages`);
264
- await installer.ensureRequiredPackages(bootstrapRequired);
265
- }
266
- else {
267
- await installer.ensureRequiredPackages();
268
- }
307
+ // npm. Default (undefined) → derive from THIS server's own package.json.
308
+ //
309
+ // We derive the list here rather than relying on
310
+ // `AddonInstaller.REQUIRED_PACKAGES` (which uses
311
+ // `require.resolve('@camstack/server/package.json')`): in the slim image the
312
+ // `@camstack/*` workspace symlinks are stripped and NODE_PATH is only
313
+ // extended to include `/repo/node_modules` LATER in this launcher, so that
314
+ // static resolves to an EMPTY list at import time — and `@camstack/system`
315
+ // (which provides the storage-provider / settings / logging infra builtins)
316
+ // would never get installed into data/addons, aborting boot with
317
+ // "No addon provides required infrastructure capability storage-provider".
318
+ // The server's own package.json is always readable next to this launcher.
319
+ const bootstrapRequired = readBootstrapRequiredAddons(dataDir, bootstrapSchema) ?? deriveBootstrapFromSelf();
320
+ console.log(`[launcher] bootstrap: ${bootstrapRequired.length} required package(s)`);
321
+ await installer.ensureRequiredPackages(bootstrapRequired);
269
322
  // Reconcile the install manifest with what is on disk. Addons baked
270
323
  // into the image are copied straight into addonsDir by the container
271
324
  // entrypoint and never pass through an install codepath, so they are
@@ -309,7 +362,11 @@ async function launch() {
309
362
  // etc); the server-local node_modules has direct deps. Listing
310
363
  // both covers every realistic resolution.
311
364
  const workspaceRootNodeModules = path.resolve(serverDir, '..', '..', 'node_modules');
312
- const extraNodePaths = [nodeModulesDir, workspaceRootNodeModules].filter((p) => fs.existsSync(p));
365
+ const frameworkPaths = (0, framework_nodepath_js_1.frameworkNodePaths)(process.env['CAMSTACK_FRAMEWORK_DIR'], fs.existsSync);
366
+ const extraNodePaths = [...frameworkPaths, nodeModulesDir, workspaceRootNodeModules].filter((p) => fs.existsSync(p));
367
+ if (frameworkPaths.length > 0) {
368
+ console.log(`[launcher] Framework dir on NODE_PATH: ${frameworkPaths[0]}`);
369
+ }
313
370
  if (extraNodePaths.length > 0) {
314
371
  const sep = process.platform === 'win32' ? ';' : ':';
315
372
  process.env['NODE_PATH'] = process.env['NODE_PATH']
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.lifecycleJobsDir = lifecycleJobsDir;
37
+ const path = __importStar(require("node:path"));
38
+ /** Single source of truth for the lifecycle job journal directory. */
39
+ function lifecycleJobsDir(dataDir) {
40
+ return path.join(dataDir, 'lifecycle', 'jobs');
41
+ }