@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.
- package/dist/api/core/cap-providers.js +38 -42
- package/dist/api/core/lifecycle-job-runner.js +157 -0
- package/dist/api/trpc/generated-cap-routers.js +32 -32
- package/dist/api/trpc/trpc.router.js +1 -1
- package/dist/boot/post-boot.service.js +25 -0
- package/dist/boot/reconcile-lifecycle-jobs.js +29 -0
- package/dist/boot/resume-framework-swap.js +119 -0
- package/dist/core/addon/addon-package.service.js +255 -17
- package/dist/core/addon/addon-registry.service.js +16 -2
- package/dist/core/agent/agent-registry.service.js +43 -1
- package/dist/core/lifecycle/lifecycle-runner.singleton.js +40 -0
- package/dist/framework-nodepath.js +49 -0
- package/dist/launcher-framework-swap.js +408 -0
- package/dist/launcher.js +75 -18
- package/dist/lifecycle-journal-path.js +41 -0
- package/dist/manual-boot.js +93 -0
- package/dist/request-framework-swap.js +41 -0
- package/package.json +1 -1
- package/dist/api/core/bulk-update-coordinator.js +0 -229
|
@@ -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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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) →
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
|
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
|
+
}
|