@camstack/agent 1.1.32 → 1.1.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-L34OOQI3.mjs +998 -0
- package/dist/chunk-L34OOQI3.mjs.map +1 -0
- package/dist/{chunk-CIITICY3.mjs → chunk-MMKRM4RT.mjs} +167 -36
- package/dist/chunk-MMKRM4RT.mjs.map +1 -0
- package/dist/cli.js +1173 -85
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +2 -1
- package/dist/cli.mjs.map +1 -1
- package/dist/index.js +1123 -35
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -1
- package/dist/starter.d.ts +2 -0
- package/dist/starter.js +1036 -0
- package/dist/starter.js.map +1 -0
- package/dist/starter.mjs +57 -0
- package/dist/starter.mjs.map +1 -0
- package/package.json +8 -3
- package/dist/chunk-CIITICY3.mjs.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __getProtoOf = Object.getPrototypeOf;
|
|
8
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
10
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
11
|
+
};
|
|
9
12
|
var __copyProps = (to, from, except, desc) => {
|
|
10
13
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
14
|
for (let key of __getOwnPropNames(from))
|
|
@@ -23,15 +26,974 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
26
|
mod
|
|
24
27
|
));
|
|
25
28
|
|
|
29
|
+
// ../node-root/dist/index.js
|
|
30
|
+
var require_dist = __commonJS({
|
|
31
|
+
"../node-root/dist/index.js"(exports2, module2) {
|
|
32
|
+
"use strict";
|
|
33
|
+
var __create2 = Object.create;
|
|
34
|
+
var __defProp2 = Object.defineProperty;
|
|
35
|
+
var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
|
|
36
|
+
var __getOwnPropNames2 = Object.getOwnPropertyNames;
|
|
37
|
+
var __getProtoOf2 = Object.getPrototypeOf;
|
|
38
|
+
var __hasOwnProp2 = Object.prototype.hasOwnProperty;
|
|
39
|
+
var __export = (target, all) => {
|
|
40
|
+
for (var name in all)
|
|
41
|
+
__defProp2(target, name, { get: all[name], enumerable: true });
|
|
42
|
+
};
|
|
43
|
+
var __copyProps2 = (to, from, except, desc) => {
|
|
44
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
45
|
+
for (let key of __getOwnPropNames2(from))
|
|
46
|
+
if (!__hasOwnProp2.call(to, key) && key !== except)
|
|
47
|
+
__defProp2(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc2(from, key)) || desc.enumerable });
|
|
48
|
+
}
|
|
49
|
+
return to;
|
|
50
|
+
};
|
|
51
|
+
var __toESM2 = (mod, isNodeMode, target) => (target = mod != null ? __create2(__getProtoOf2(mod)) : {}, __copyProps2(
|
|
52
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
53
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
54
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
55
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
56
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp2(target, "default", { value: mod, enumerable: true }) : target,
|
|
57
|
+
mod
|
|
58
|
+
));
|
|
59
|
+
var __toCommonJS = (mod) => __copyProps2(__defProp2({}, "__esModule", { value: true }), mod);
|
|
60
|
+
var src_exports = {};
|
|
61
|
+
__export(src_exports, {
|
|
62
|
+
AGENT_ROOT_SPEC: () => AGENT_ROOT_SPEC2,
|
|
63
|
+
HOST_EXTERNAL_SPECIFIERS: () => HOST_EXTERNAL_SPECIFIERS,
|
|
64
|
+
HUB_ROOT_SPEC: () => HUB_ROOT_SPEC,
|
|
65
|
+
RESTART_INTENT_FILE: () => RESTART_INTENT_FILE,
|
|
66
|
+
RootUpdateService: () => RootUpdateService2,
|
|
67
|
+
SERVER_ROOT_DIRNAME: () => SERVER_ROOT_DIRNAME,
|
|
68
|
+
SERVER_ROOT_STATE_FILE: () => SERVER_ROOT_STATE_FILE,
|
|
69
|
+
clearRestartIntentMarker: () => clearRestartIntentMarker,
|
|
70
|
+
compareSemver: () => compareSemver,
|
|
71
|
+
detectWorkspaceRoot: () => detectWorkspaceRoot,
|
|
72
|
+
emptyServerRootState: () => emptyServerRootState,
|
|
73
|
+
isHostExternal: () => isHostExternal,
|
|
74
|
+
isServerRootState: () => isServerRootState,
|
|
75
|
+
minNodeMajorOf: () => minNodeMajorOf,
|
|
76
|
+
planBoot: () => planBoot,
|
|
77
|
+
readRestartIntentMarker: () => readRestartIntentMarker,
|
|
78
|
+
readServerRootState: () => readServerRootState,
|
|
79
|
+
registerActiveRootResolver: () => registerActiveRootResolver,
|
|
80
|
+
restartIntentMarkerPath: () => restartIntentMarkerPath,
|
|
81
|
+
rootEntryPath: () => rootEntryPath,
|
|
82
|
+
rootPackageDir: () => rootPackageDir,
|
|
83
|
+
runNodeStarter: () => runNodeStarter,
|
|
84
|
+
serverRootDir: () => serverRootDir2,
|
|
85
|
+
stateFilePath: () => stateFilePath,
|
|
86
|
+
validateVersionDir: () => validateVersionDir,
|
|
87
|
+
versionDir: () => versionDir,
|
|
88
|
+
versionsDir: () => versionsDir,
|
|
89
|
+
writeRestartIntentMarker: () => writeRestartIntentMarker2,
|
|
90
|
+
writeServerRootState: () => writeServerRootState
|
|
91
|
+
});
|
|
92
|
+
module2.exports = __toCommonJS(src_exports);
|
|
93
|
+
var HUB_ROOT_SPEC = {
|
|
94
|
+
packageName: "@camstack/server",
|
|
95
|
+
entryRelPath: ["dist", "launcher.js"]
|
|
96
|
+
};
|
|
97
|
+
var AGENT_ROOT_SPEC2 = {
|
|
98
|
+
packageName: "@camstack/agent",
|
|
99
|
+
entryRelPath: ["dist", "cli.js"]
|
|
100
|
+
};
|
|
101
|
+
var fs8 = __toESM2(require("fs"));
|
|
102
|
+
var path9 = __toESM2(require("path"));
|
|
103
|
+
var SERVER_ROOT_DIRNAME = "server-root";
|
|
104
|
+
var SERVER_ROOT_STATE_FILE = "state.json";
|
|
105
|
+
var RESTART_INTENT_FILE = ".restart-intent";
|
|
106
|
+
function emptyServerRootState() {
|
|
107
|
+
return {
|
|
108
|
+
schemaVersion: 1,
|
|
109
|
+
currentVersion: null,
|
|
110
|
+
previousVersion: null,
|
|
111
|
+
pendingBoot: null,
|
|
112
|
+
rolledBack: null
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function serverRootDir2(dataDir) {
|
|
116
|
+
return path9.join(dataDir, SERVER_ROOT_DIRNAME);
|
|
117
|
+
}
|
|
118
|
+
function versionsDir(rootDir) {
|
|
119
|
+
return path9.join(rootDir, "versions");
|
|
120
|
+
}
|
|
121
|
+
function versionDir(rootDir, version) {
|
|
122
|
+
return path9.join(versionsDir(rootDir), version);
|
|
123
|
+
}
|
|
124
|
+
function rootPackageDir(versionDirPath, spec) {
|
|
125
|
+
return path9.join(versionDirPath, "node_modules", ...spec.packageName.split("/"));
|
|
126
|
+
}
|
|
127
|
+
function rootEntryPath(versionDirPath, spec) {
|
|
128
|
+
return path9.join(rootPackageDir(versionDirPath, spec), ...spec.entryRelPath);
|
|
129
|
+
}
|
|
130
|
+
function stateFilePath(rootDir) {
|
|
131
|
+
return path9.join(rootDir, SERVER_ROOT_STATE_FILE);
|
|
132
|
+
}
|
|
133
|
+
function isNullableString(v) {
|
|
134
|
+
return v === null || typeof v === "string";
|
|
135
|
+
}
|
|
136
|
+
function isPendingBoot(v) {
|
|
137
|
+
if (typeof v !== "object" || v === null) return false;
|
|
138
|
+
const p = v;
|
|
139
|
+
return typeof p["version"] === "string" && isNullableString(p["fromVersion"]) && typeof p["requestedAtMs"] === "number" && typeof p["bootAttempts"] === "number";
|
|
140
|
+
}
|
|
141
|
+
function isRollbackInfo(v) {
|
|
142
|
+
if (typeof v !== "object" || v === null) return false;
|
|
143
|
+
const r = v;
|
|
144
|
+
return typeof r["fromVersion"] === "string" && isNullableString(r["toVersion"]) && typeof r["atMs"] === "number" && typeof r["reason"] === "string";
|
|
145
|
+
}
|
|
146
|
+
function isServerRootState(v) {
|
|
147
|
+
if (typeof v !== "object" || v === null) return false;
|
|
148
|
+
const s = v;
|
|
149
|
+
if (s["schemaVersion"] !== 1) return false;
|
|
150
|
+
if (!isNullableString(s["currentVersion"])) return false;
|
|
151
|
+
if (!isNullableString(s["previousVersion"])) return false;
|
|
152
|
+
if (s["pendingBoot"] !== null && !isPendingBoot(s["pendingBoot"])) return false;
|
|
153
|
+
if (s["rolledBack"] !== null && !isRollbackInfo(s["rolledBack"])) return false;
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
function readServerRootState(rootDir) {
|
|
157
|
+
try {
|
|
158
|
+
const raw = JSON.parse(fs8.readFileSync(stateFilePath(rootDir), "utf-8"));
|
|
159
|
+
return isServerRootState(raw) ? raw : null;
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function writeServerRootState(rootDir, state) {
|
|
165
|
+
fs8.mkdirSync(rootDir, { recursive: true });
|
|
166
|
+
const target = stateFilePath(rootDir);
|
|
167
|
+
const tmp = `${target}.tmp`;
|
|
168
|
+
fs8.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf-8");
|
|
169
|
+
fs8.renameSync(tmp, target);
|
|
170
|
+
}
|
|
171
|
+
function restartIntentMarkerPath(rootDir) {
|
|
172
|
+
return path9.join(rootDir, RESTART_INTENT_FILE);
|
|
173
|
+
}
|
|
174
|
+
function isRestartIntentMarker(v) {
|
|
175
|
+
if (typeof v !== "object" || v === null) return false;
|
|
176
|
+
const m = v;
|
|
177
|
+
return typeof m["requestedAtMs"] === "number" && typeof m["reason"] === "string";
|
|
178
|
+
}
|
|
179
|
+
function writeRestartIntentMarker2(rootDir, marker) {
|
|
180
|
+
fs8.mkdirSync(rootDir, { recursive: true });
|
|
181
|
+
const target = restartIntentMarkerPath(rootDir);
|
|
182
|
+
const tmp = `${target}.tmp`;
|
|
183
|
+
fs8.writeFileSync(tmp, JSON.stringify(marker, null, 2), "utf-8");
|
|
184
|
+
fs8.renameSync(tmp, target);
|
|
185
|
+
}
|
|
186
|
+
function readRestartIntentMarker(rootDir) {
|
|
187
|
+
try {
|
|
188
|
+
const raw = JSON.parse(fs8.readFileSync(restartIntentMarkerPath(rootDir), "utf-8"));
|
|
189
|
+
return isRestartIntentMarker(raw) ? raw : null;
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function clearRestartIntentMarker(rootDir) {
|
|
195
|
+
try {
|
|
196
|
+
fs8.rmSync(restartIntentMarkerPath(rootDir), { force: true });
|
|
197
|
+
} catch {
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function minNodeMajorOf(enginesNode) {
|
|
201
|
+
const match = /(\d+)/.exec(enginesNode);
|
|
202
|
+
if (match === null) return null;
|
|
203
|
+
const major = Number.parseInt(match[1] ?? "", 10);
|
|
204
|
+
return Number.isNaN(major) ? null : major;
|
|
205
|
+
}
|
|
206
|
+
function validateVersionDir(rootDir, version, nodeMajor, spec) {
|
|
207
|
+
const vDir = versionDir(rootDir, version);
|
|
208
|
+
const entry = rootEntryPath(vDir, spec);
|
|
209
|
+
if (!fs8.existsSync(entry)) {
|
|
210
|
+
return `root entry missing: ${entry}`;
|
|
211
|
+
}
|
|
212
|
+
const pkgJsonPath = path9.join(rootPackageDir(vDir, spec), "package.json");
|
|
213
|
+
let pkg;
|
|
214
|
+
try {
|
|
215
|
+
const raw = JSON.parse(fs8.readFileSync(pkgJsonPath, "utf-8"));
|
|
216
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
217
|
+
return `package.json malformed: ${pkgJsonPath}`;
|
|
218
|
+
}
|
|
219
|
+
pkg = raw;
|
|
220
|
+
} catch {
|
|
221
|
+
return `package.json unreadable: ${pkgJsonPath}`;
|
|
222
|
+
}
|
|
223
|
+
if (pkg["name"] !== spec.packageName) {
|
|
224
|
+
return `package name mismatch: expected ${spec.packageName}, got ${String(pkg["name"])}`;
|
|
225
|
+
}
|
|
226
|
+
if (pkg["version"] !== version) {
|
|
227
|
+
return `package version mismatch: dir says ${version}, package.json says ${String(pkg["version"])}`;
|
|
228
|
+
}
|
|
229
|
+
const engines = pkg["engines"];
|
|
230
|
+
if (typeof engines === "object" && engines !== null) {
|
|
231
|
+
const enginesNode = engines["node"];
|
|
232
|
+
if (typeof enginesNode === "string") {
|
|
233
|
+
const minMajor = minNodeMajorOf(enginesNode);
|
|
234
|
+
if (minMajor !== null && nodeMajor < minMajor) {
|
|
235
|
+
return `engines.node "${enginesNode}" requires Node >= ${minMajor}, runtime is ${nodeMajor}`;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
function planBoot(state, isValidVersion, now) {
|
|
242
|
+
if (state === null) {
|
|
243
|
+
return { kind: "baked", reason: "no server-root state", stateToWrite: null };
|
|
244
|
+
}
|
|
245
|
+
let next = state;
|
|
246
|
+
let changed = false;
|
|
247
|
+
if (next.pendingBoot !== null) {
|
|
248
|
+
const pending = next.pendingBoot;
|
|
249
|
+
if (pending.bootAttempts >= 1) {
|
|
250
|
+
next = {
|
|
251
|
+
...next,
|
|
252
|
+
pendingBoot: null,
|
|
253
|
+
rolledBack: {
|
|
254
|
+
fromVersion: pending.version,
|
|
255
|
+
toVersion: next.currentVersion,
|
|
256
|
+
atMs: now(),
|
|
257
|
+
reason: "probation boot did not reach ready \u2014 rolled back"
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
changed = true;
|
|
261
|
+
} else if (!isValidVersion(pending.version)) {
|
|
262
|
+
next = {
|
|
263
|
+
...next,
|
|
264
|
+
pendingBoot: null,
|
|
265
|
+
rolledBack: {
|
|
266
|
+
fromVersion: pending.version,
|
|
267
|
+
toVersion: next.currentVersion,
|
|
268
|
+
atMs: now(),
|
|
269
|
+
reason: "staged version failed validation"
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
changed = true;
|
|
273
|
+
} else {
|
|
274
|
+
return {
|
|
275
|
+
kind: "data-root",
|
|
276
|
+
version: pending.version,
|
|
277
|
+
probation: true,
|
|
278
|
+
stateToWrite: {
|
|
279
|
+
...next,
|
|
280
|
+
pendingBoot: { ...pending, bootAttempts: pending.bootAttempts + 1 }
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (next.currentVersion !== null) {
|
|
286
|
+
if (isValidVersion(next.currentVersion)) {
|
|
287
|
+
return {
|
|
288
|
+
kind: "data-root",
|
|
289
|
+
version: next.currentVersion,
|
|
290
|
+
probation: false,
|
|
291
|
+
stateToWrite: changed ? next : null
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
const broken = next.currentVersion;
|
|
295
|
+
if (next.previousVersion !== null && isValidVersion(next.previousVersion)) {
|
|
296
|
+
const fallback = next.previousVersion;
|
|
297
|
+
return {
|
|
298
|
+
kind: "data-root",
|
|
299
|
+
version: fallback,
|
|
300
|
+
probation: false,
|
|
301
|
+
stateToWrite: {
|
|
302
|
+
...next,
|
|
303
|
+
currentVersion: fallback,
|
|
304
|
+
previousVersion: null,
|
|
305
|
+
rolledBack: {
|
|
306
|
+
fromVersion: broken,
|
|
307
|
+
toVersion: fallback,
|
|
308
|
+
atMs: now(),
|
|
309
|
+
reason: "active version failed validation"
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
kind: "baked",
|
|
316
|
+
reason: `active version ${broken} failed validation and no valid previous version exists`,
|
|
317
|
+
stateToWrite: {
|
|
318
|
+
...next,
|
|
319
|
+
currentVersion: null,
|
|
320
|
+
rolledBack: {
|
|
321
|
+
fromVersion: broken,
|
|
322
|
+
toVersion: null,
|
|
323
|
+
atMs: now(),
|
|
324
|
+
reason: "active version failed validation"
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
kind: "baked",
|
|
331
|
+
reason: "no active data-root version",
|
|
332
|
+
stateToWrite: changed ? next : null
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
function parse(version) {
|
|
336
|
+
const dashIdx = version.indexOf("-");
|
|
337
|
+
const base = dashIdx === -1 ? version : version.slice(0, dashIdx);
|
|
338
|
+
const prerelease = dashIdx === -1 ? null : version.slice(dashIdx + 1);
|
|
339
|
+
const nums = base.split(".").map((seg) => {
|
|
340
|
+
const n = Number.parseInt(seg, 10);
|
|
341
|
+
return Number.isNaN(n) ? 0 : n;
|
|
342
|
+
});
|
|
343
|
+
return { nums, prerelease };
|
|
344
|
+
}
|
|
345
|
+
function compareSemver(a, b) {
|
|
346
|
+
const pa = parse(a);
|
|
347
|
+
const pb = parse(b);
|
|
348
|
+
const len = Math.max(pa.nums.length, pb.nums.length);
|
|
349
|
+
for (let i = 0; i < len; i++) {
|
|
350
|
+
const na = pa.nums[i] ?? 0;
|
|
351
|
+
const nb = pb.nums[i] ?? 0;
|
|
352
|
+
if (na < nb) return -1;
|
|
353
|
+
if (na > nb) return 1;
|
|
354
|
+
}
|
|
355
|
+
if (pa.prerelease === null && pb.prerelease === null) return 0;
|
|
356
|
+
if (pa.prerelease === null) return 1;
|
|
357
|
+
if (pb.prerelease === null) return -1;
|
|
358
|
+
if (pa.prerelease < pb.prerelease) return -1;
|
|
359
|
+
if (pa.prerelease > pb.prerelease) return 1;
|
|
360
|
+
return 0;
|
|
361
|
+
}
|
|
362
|
+
var fs22 = __toESM2(require("fs"));
|
|
363
|
+
var path22 = __toESM2(require("path"));
|
|
364
|
+
function detectWorkspaceRoot(fromDir) {
|
|
365
|
+
let dir = path22.resolve(fromDir);
|
|
366
|
+
for (; ; ) {
|
|
367
|
+
const pkgPath = path22.join(dir, "package.json");
|
|
368
|
+
try {
|
|
369
|
+
const raw = JSON.parse(fs22.readFileSync(pkgPath, "utf-8"));
|
|
370
|
+
if (typeof raw === "object" && raw !== null && raw["workspaces"] !== void 0) {
|
|
371
|
+
return dir;
|
|
372
|
+
}
|
|
373
|
+
} catch {
|
|
374
|
+
}
|
|
375
|
+
const parent = path22.dirname(dir);
|
|
376
|
+
if (parent === dir) return null;
|
|
377
|
+
dir = parent;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
var path32 = __toESM2(require("path"));
|
|
381
|
+
var import_node_url = require("url");
|
|
382
|
+
var HOST_EXTERNAL_SPECIFIERS = [
|
|
383
|
+
"@camstack/system",
|
|
384
|
+
"@camstack/shm-ring",
|
|
385
|
+
"@camstack/types",
|
|
386
|
+
"@camstack/sdk",
|
|
387
|
+
"zod",
|
|
388
|
+
"@trpc/server",
|
|
389
|
+
"@trpc/client",
|
|
390
|
+
"sharp",
|
|
391
|
+
"node-av"
|
|
392
|
+
];
|
|
393
|
+
function isHostExternal(specifier) {
|
|
394
|
+
return HOST_EXTERNAL_SPECIFIERS.some(
|
|
395
|
+
(name) => specifier === name || specifier.startsWith(`${name}/`)
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
function registerActiveRootResolver(activeRootDir) {
|
|
399
|
+
const moduleApi = require("module");
|
|
400
|
+
const registerHooks = moduleApi["registerHooks"];
|
|
401
|
+
if (typeof registerHooks !== "function") {
|
|
402
|
+
console.warn(
|
|
403
|
+
"[starter] module.registerHooks unavailable (Node < 22.15) \u2014 host-external redirect skipped"
|
|
404
|
+
);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const anchorURL = (0, import_node_url.pathToFileURL)(
|
|
408
|
+
path32.join(activeRootDir, "node_modules", "__camstack_starter_anchor__.js")
|
|
409
|
+
).href;
|
|
410
|
+
const hooks = {
|
|
411
|
+
resolve: (specifier, context, nextResolve) => {
|
|
412
|
+
if (isHostExternal(specifier)) {
|
|
413
|
+
try {
|
|
414
|
+
return nextResolve(specifier, { ...context, parentURL: anchorURL });
|
|
415
|
+
} catch {
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return nextResolve(specifier, context);
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
registerHooks(hooks);
|
|
422
|
+
}
|
|
423
|
+
function runNodeStarter(options) {
|
|
424
|
+
const env = options.env ?? process.env;
|
|
425
|
+
const now = options.now ?? Date.now;
|
|
426
|
+
const registerResolver = options.registerResolver ?? registerActiveRootResolver;
|
|
427
|
+
const disabled = env[options.envNames.killSwitch] === "off";
|
|
428
|
+
const workspaceRoot = detectWorkspaceRoot(options.starterDir);
|
|
429
|
+
if (workspaceRoot !== null) {
|
|
430
|
+
console.log(
|
|
431
|
+
`[starter] workspace checkout detected at ${workspaceRoot} \u2014 using plain resolution`
|
|
432
|
+
);
|
|
433
|
+
env[options.envNames.bootMode] = "workspace";
|
|
434
|
+
options.loadEntry(options.seedEntry);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (disabled) {
|
|
438
|
+
console.log(`[starter] ${options.envNames.killSwitch}=off \u2014 booting the baked seed closure`);
|
|
439
|
+
env[options.envNames.bootMode] = "baked";
|
|
440
|
+
options.loadEntry(options.seedEntry);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const rootDir = serverRootDir2(options.dataDir);
|
|
444
|
+
const state = readServerRootState(rootDir);
|
|
445
|
+
const nodeMajor = options.nodeMajor ?? Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
|
|
446
|
+
const isValidVersion = (version) => {
|
|
447
|
+
const reason = validateVersionDir(rootDir, version, nodeMajor, options.spec);
|
|
448
|
+
if (reason !== null) console.warn(`[starter] version ${version} invalid: ${reason}`);
|
|
449
|
+
return reason === null;
|
|
450
|
+
};
|
|
451
|
+
const plan = planBoot(state, isValidVersion, now);
|
|
452
|
+
if (plan.stateToWrite !== null) {
|
|
453
|
+
try {
|
|
454
|
+
writeServerRootState(rootDir, plan.stateToWrite);
|
|
455
|
+
} catch (err) {
|
|
456
|
+
console.error("[starter] FAILED to persist server-root state \u2014 booting baked seed:", err);
|
|
457
|
+
env[options.envNames.bootMode] = "baked";
|
|
458
|
+
options.loadEntry(options.seedEntry);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (plan.stateToWrite.rolledBack !== null && (state?.rolledBack ?? null) !== plan.stateToWrite.rolledBack) {
|
|
462
|
+
const rb = plan.stateToWrite.rolledBack;
|
|
463
|
+
console.warn(
|
|
464
|
+
`[starter] ROLLED BACK ${rb.fromVersion} -> ${rb.toVersion ?? "baked seed"}: ${rb.reason}`
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (plan.kind === "data-root") {
|
|
469
|
+
const activeRootDir = versionDir(rootDir, plan.version);
|
|
470
|
+
const entry = rootEntryPath(activeRootDir, options.spec);
|
|
471
|
+
console.log(
|
|
472
|
+
`[starter] booting ${options.spec.packageName}@${plan.version} from ${activeRootDir}` + (plan.probation ? " (probation boot \u2014 health-check armed)" : "")
|
|
473
|
+
);
|
|
474
|
+
env[options.envNames.bootMode] = "data-root";
|
|
475
|
+
env[options.envNames.activeRoot] = activeRootDir;
|
|
476
|
+
env[options.envNames.activeVersion] = plan.version;
|
|
477
|
+
registerResolver(activeRootDir);
|
|
478
|
+
options.loadEntry(entry);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
console.log(`[starter] booting baked seed closure (${plan.reason})`);
|
|
482
|
+
env[options.envNames.bootMode] = "baked";
|
|
483
|
+
options.loadEntry(options.seedEntry);
|
|
484
|
+
}
|
|
485
|
+
var import_node_child_process = require("child_process");
|
|
486
|
+
var fs32 = __toESM2(require("fs"));
|
|
487
|
+
var path42 = __toESM2(require("path"));
|
|
488
|
+
var import_node_util = require("util");
|
|
489
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
490
|
+
function buildNpmRegistryArgs(registry) {
|
|
491
|
+
if (registry === void 0 || registry.length === 0) return [];
|
|
492
|
+
return ["--registry", registry, `--@camstack:registry=${registry}`];
|
|
493
|
+
}
|
|
494
|
+
var NPM_VIEW_TIMEOUT_MS = 2e4;
|
|
495
|
+
var NPM_INSTALL_TIMEOUT_MS = 15 * 6e4;
|
|
496
|
+
var RESTART_REASON_PREFIX = "server-update";
|
|
497
|
+
function readPackageVersion(pkgJsonPath) {
|
|
498
|
+
try {
|
|
499
|
+
const raw = JSON.parse(fs32.readFileSync(pkgJsonPath, "utf-8"));
|
|
500
|
+
if (typeof raw === "object" && raw !== null) {
|
|
501
|
+
const version = raw["version"];
|
|
502
|
+
if (typeof version === "string") return version;
|
|
503
|
+
}
|
|
504
|
+
} catch {
|
|
505
|
+
}
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
var RootUpdateService2 = class _RootUpdateService {
|
|
509
|
+
spec;
|
|
510
|
+
envNames;
|
|
511
|
+
logger;
|
|
512
|
+
restartServerFn;
|
|
513
|
+
dataDir;
|
|
514
|
+
execNpm;
|
|
515
|
+
env;
|
|
516
|
+
now;
|
|
517
|
+
runningPackageJsonPath;
|
|
518
|
+
workspaceProbeDir;
|
|
519
|
+
checkCache = null;
|
|
520
|
+
inFlight = "idle";
|
|
521
|
+
constructor(options) {
|
|
522
|
+
this.spec = options.spec;
|
|
523
|
+
this.envNames = options.envNames;
|
|
524
|
+
this.logger = options.logger;
|
|
525
|
+
this.restartServerFn = options.restartServer;
|
|
526
|
+
this.dataDir = path42.resolve(options.dataDir);
|
|
527
|
+
this.execNpm = options.execNpm ?? (async (args2, opts) => {
|
|
528
|
+
const { stdout } = await execFileAsync("npm", [...args2], {
|
|
529
|
+
cwd: opts.cwd,
|
|
530
|
+
timeout: opts.timeoutMs
|
|
531
|
+
});
|
|
532
|
+
return { stdout };
|
|
533
|
+
});
|
|
534
|
+
this.env = options.env ?? process.env;
|
|
535
|
+
this.now = options.now ?? Date.now;
|
|
536
|
+
this.runningPackageJsonPath = options.runningPackageJsonPath;
|
|
537
|
+
this.workspaceProbeDir = options.workspaceProbeDir;
|
|
538
|
+
}
|
|
539
|
+
// ── Status ────────────────────────────────────────────────────────────
|
|
540
|
+
resolveBootMode() {
|
|
541
|
+
const raw = this.env[this.envNames.bootMode];
|
|
542
|
+
if (raw === "workspace" || raw === "baked" || raw === "data-root") return raw;
|
|
543
|
+
return detectWorkspaceRoot(this.workspaceProbeDir) !== null ? "workspace" : "baked";
|
|
544
|
+
}
|
|
545
|
+
runningVersion() {
|
|
546
|
+
return readPackageVersion(this.runningPackageJsonPath);
|
|
547
|
+
}
|
|
548
|
+
/** The running root package's identity — feeds registerNode / topology rows. */
|
|
549
|
+
getRunningRootPackage() {
|
|
550
|
+
return { name: this.spec.packageName, version: this.runningVersion() };
|
|
551
|
+
}
|
|
552
|
+
seedVersion() {
|
|
553
|
+
const seedDir = this.env[this.envNames.seedDir];
|
|
554
|
+
if (seedDir === void 0 || seedDir.length === 0) return null;
|
|
555
|
+
return readPackageVersion(path42.join(seedDir, "package.json"));
|
|
556
|
+
}
|
|
557
|
+
rootDir() {
|
|
558
|
+
return serverRootDir2(this.dataDir);
|
|
559
|
+
}
|
|
560
|
+
readState() {
|
|
561
|
+
return readServerRootState(this.rootDir()) ?? emptyServerRootState();
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Like `readState`, but distinguishes "state.json missing" (normal on a
|
|
565
|
+
* fresh node) from "state.json present but corrupt" — in the corrupt case
|
|
566
|
+
* the starter booted the baked seed while installed data-dir versions are
|
|
567
|
+
* silently ignored, which the status surface must make visible.
|
|
568
|
+
*/
|
|
569
|
+
readStateInfo() {
|
|
570
|
+
const rootDir = this.rootDir();
|
|
571
|
+
const raw = readServerRootState(rootDir);
|
|
572
|
+
if (raw !== null) return { state: raw, corrupt: false };
|
|
573
|
+
return { state: emptyServerRootState(), corrupt: fs32.existsSync(stateFilePath(rootDir)) };
|
|
574
|
+
}
|
|
575
|
+
updateState(state) {
|
|
576
|
+
if (this.inFlight === "checking") return "checking";
|
|
577
|
+
if (this.inFlight === "staging") return "staging";
|
|
578
|
+
if (state.pendingBoot !== null) {
|
|
579
|
+
const activeVersion = this.env[this.envNames.activeVersion] ?? null;
|
|
580
|
+
if (activeVersion !== null && activeVersion === state.pendingBoot.version) {
|
|
581
|
+
return "awaiting-confirmation";
|
|
582
|
+
}
|
|
583
|
+
return "pending-restart";
|
|
584
|
+
}
|
|
585
|
+
return "idle";
|
|
586
|
+
}
|
|
587
|
+
async getServerPackageStatus() {
|
|
588
|
+
const { state, corrupt } = this.readStateInfo();
|
|
589
|
+
const runningVersion = this.runningVersion();
|
|
590
|
+
const latestVersion = this.checkCache?.latestVersion ?? null;
|
|
591
|
+
const updateAvailable = latestVersion !== null && runningVersion !== null && compareSemver(latestVersion, runningVersion) > 0;
|
|
592
|
+
return {
|
|
593
|
+
packageName: this.spec.packageName,
|
|
594
|
+
runningVersion,
|
|
595
|
+
nodeRuntimeVersion: process.versions.node,
|
|
596
|
+
activeVersion: this.env[this.envNames.activeVersion] ?? state.currentVersion,
|
|
597
|
+
previousVersion: state.previousVersion,
|
|
598
|
+
seedVersion: this.seedVersion(),
|
|
599
|
+
latestVersion,
|
|
600
|
+
updateAvailable,
|
|
601
|
+
bootMode: this.resolveBootMode(),
|
|
602
|
+
updateState: this.updateState(state),
|
|
603
|
+
pendingVersion: state.pendingBoot?.version ?? null,
|
|
604
|
+
rolledBack: state.rolledBack,
|
|
605
|
+
stateFileCorrupt: corrupt,
|
|
606
|
+
lastCheckedAtMs: this.checkCache?.checkedAtMs ?? null
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
// ── Check ─────────────────────────────────────────────────────────────
|
|
610
|
+
async checkServerUpdate() {
|
|
611
|
+
const runningVersion = this.runningVersion();
|
|
612
|
+
if (this.inFlight !== "idle") {
|
|
613
|
+
return {
|
|
614
|
+
packageName: this.spec.packageName,
|
|
615
|
+
runningVersion,
|
|
616
|
+
latestVersion: this.checkCache?.latestVersion ?? null,
|
|
617
|
+
updateAvailable: false,
|
|
618
|
+
checkedAtMs: this.checkCache?.checkedAtMs ?? this.now(),
|
|
619
|
+
error: `busy: ${this.inFlight}`
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
this.inFlight = "checking";
|
|
623
|
+
try {
|
|
624
|
+
const registry = this.env["CAMSTACK_NPM_REGISTRY"];
|
|
625
|
+
const args2 = [
|
|
626
|
+
"view",
|
|
627
|
+
`${this.spec.packageName}@latest`,
|
|
628
|
+
"version",
|
|
629
|
+
...buildNpmRegistryArgs(registry)
|
|
630
|
+
];
|
|
631
|
+
const { stdout } = await this.execNpm(args2, { timeoutMs: NPM_VIEW_TIMEOUT_MS });
|
|
632
|
+
const latestVersion = stdout.trim().length > 0 ? stdout.trim() : null;
|
|
633
|
+
this.checkCache = { latestVersion, checkedAtMs: this.now(), error: null };
|
|
634
|
+
const updateAvailable = latestVersion !== null && runningVersion !== null && compareSemver(latestVersion, runningVersion) > 0;
|
|
635
|
+
return {
|
|
636
|
+
packageName: this.spec.packageName,
|
|
637
|
+
runningVersion,
|
|
638
|
+
latestVersion,
|
|
639
|
+
updateAvailable,
|
|
640
|
+
checkedAtMs: this.checkCache.checkedAtMs,
|
|
641
|
+
error: null
|
|
642
|
+
};
|
|
643
|
+
} catch (err) {
|
|
644
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
645
|
+
this.checkCache = {
|
|
646
|
+
latestVersion: this.checkCache?.latestVersion ?? null,
|
|
647
|
+
checkedAtMs: this.now(),
|
|
648
|
+
error: message
|
|
649
|
+
};
|
|
650
|
+
this.logger.warn("root package update check failed", { meta: { error: message } });
|
|
651
|
+
return {
|
|
652
|
+
packageName: this.spec.packageName,
|
|
653
|
+
runningVersion,
|
|
654
|
+
latestVersion: this.checkCache.latestVersion,
|
|
655
|
+
updateAvailable: false,
|
|
656
|
+
checkedAtMs: this.checkCache.checkedAtMs,
|
|
657
|
+
error: message
|
|
658
|
+
};
|
|
659
|
+
} finally {
|
|
660
|
+
this.inFlight = "idle";
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
// ── Apply ─────────────────────────────────────────────────────────────
|
|
664
|
+
async applyServerUpdate(input) {
|
|
665
|
+
const bootMode = this.resolveBootMode();
|
|
666
|
+
if (bootMode === "workspace") {
|
|
667
|
+
return {
|
|
668
|
+
accepted: false,
|
|
669
|
+
targetVersion: input.version ?? null,
|
|
670
|
+
restarting: false,
|
|
671
|
+
message: "Refused: running from a workspace checkout \u2014 the dev loop resolves code from the workspace, a data-root update would never be loaded."
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
if (this.inFlight !== "idle") {
|
|
675
|
+
return {
|
|
676
|
+
accepted: false,
|
|
677
|
+
targetVersion: input.version ?? null,
|
|
678
|
+
restarting: false,
|
|
679
|
+
message: `Refused: another operation is in flight (${this.inFlight}).`
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
const stateBefore = this.readState();
|
|
683
|
+
if (stateBefore.pendingBoot !== null) {
|
|
684
|
+
return {
|
|
685
|
+
accepted: false,
|
|
686
|
+
targetVersion: input.version ?? null,
|
|
687
|
+
restarting: false,
|
|
688
|
+
message: `Refused: version ${stateBefore.pendingBoot.version} is already staged and awaiting restart.`
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
let target = input.version ?? null;
|
|
692
|
+
if (target === null) {
|
|
693
|
+
const check = await this.checkServerUpdate();
|
|
694
|
+
if (check.error !== null || check.latestVersion === null) {
|
|
695
|
+
return {
|
|
696
|
+
accepted: false,
|
|
697
|
+
targetVersion: null,
|
|
698
|
+
restarting: false,
|
|
699
|
+
message: `Refused: could not resolve the latest version (${check.error ?? "no version returned"}).`
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
target = check.latestVersion;
|
|
703
|
+
}
|
|
704
|
+
const runningVersion = this.runningVersion();
|
|
705
|
+
if (target === runningVersion) {
|
|
706
|
+
return {
|
|
707
|
+
accepted: false,
|
|
708
|
+
targetVersion: target,
|
|
709
|
+
restarting: false,
|
|
710
|
+
message: `Refused: ${this.spec.packageName}@${target} is already running.`
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
this.inFlight = "staging";
|
|
714
|
+
try {
|
|
715
|
+
await this.stageAndActivate(target);
|
|
716
|
+
} catch (err) {
|
|
717
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
718
|
+
this.logger.error("root package update staging failed", {
|
|
719
|
+
meta: { targetVersion: target, error: message }
|
|
720
|
+
});
|
|
721
|
+
return {
|
|
722
|
+
accepted: false,
|
|
723
|
+
targetVersion: target,
|
|
724
|
+
restarting: false,
|
|
725
|
+
message: `Staging failed: ${message}`
|
|
726
|
+
};
|
|
727
|
+
} finally {
|
|
728
|
+
this.inFlight = "idle";
|
|
729
|
+
}
|
|
730
|
+
const state = this.readState();
|
|
731
|
+
writeServerRootState(this.rootDir(), {
|
|
732
|
+
...state,
|
|
733
|
+
pendingBoot: {
|
|
734
|
+
version: target,
|
|
735
|
+
fromVersion: state.currentVersion,
|
|
736
|
+
requestedAtMs: this.now(),
|
|
737
|
+
bootAttempts: 0
|
|
738
|
+
},
|
|
739
|
+
rolledBack: null
|
|
740
|
+
});
|
|
741
|
+
this.logger.info("root package update staged \u2014 restarting to apply", {
|
|
742
|
+
meta: { targetVersion: target, fromVersion: state.currentVersion }
|
|
743
|
+
});
|
|
744
|
+
this.restartServerFn(`${RESTART_REASON_PREFIX}: ${this.spec.packageName}@${target}`);
|
|
745
|
+
return {
|
|
746
|
+
accepted: true,
|
|
747
|
+
targetVersion: target,
|
|
748
|
+
restarting: true,
|
|
749
|
+
message: `Staged ${this.spec.packageName}@${target} \u2014 restarting to apply (probation boot with auto-rollback).`
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* npm-install the target closure into a SAME-FS staging dir inside
|
|
754
|
+
* `versions/`, validate it, then atomically rename it into place.
|
|
755
|
+
*/
|
|
756
|
+
async stageAndActivate(target) {
|
|
757
|
+
const rootDir = this.rootDir();
|
|
758
|
+
const vDir = versionsDir(rootDir);
|
|
759
|
+
fs32.mkdirSync(vDir, { recursive: true });
|
|
760
|
+
const stagingDir = path42.join(vDir, `.staging-${target}-${process.pid}-${this.now()}`);
|
|
761
|
+
fs32.mkdirSync(stagingDir, { recursive: true });
|
|
762
|
+
try {
|
|
763
|
+
fs32.writeFileSync(
|
|
764
|
+
path42.join(stagingDir, "package.json"),
|
|
765
|
+
JSON.stringify({ name: "camstack-node-root", private: true }, null, 2),
|
|
766
|
+
"utf-8"
|
|
767
|
+
);
|
|
768
|
+
const registry = this.env["CAMSTACK_NPM_REGISTRY"];
|
|
769
|
+
await this.execNpm(
|
|
770
|
+
[
|
|
771
|
+
"install",
|
|
772
|
+
"--omit=dev",
|
|
773
|
+
"--no-audit",
|
|
774
|
+
"--no-fund",
|
|
775
|
+
"--loglevel=error",
|
|
776
|
+
`${this.spec.packageName}@${target}`,
|
|
777
|
+
...buildNpmRegistryArgs(registry)
|
|
778
|
+
],
|
|
779
|
+
{ cwd: stagingDir, timeoutMs: NPM_INSTALL_TIMEOUT_MS }
|
|
780
|
+
);
|
|
781
|
+
const entry = rootEntryPath(stagingDir, this.spec);
|
|
782
|
+
if (!fs32.existsSync(entry)) {
|
|
783
|
+
throw new Error(`staged closure is missing the root entry (${entry})`);
|
|
784
|
+
}
|
|
785
|
+
const stagedVersion = readPackageVersion(
|
|
786
|
+
path42.join(rootPackageDir(stagingDir, this.spec), "package.json")
|
|
787
|
+
);
|
|
788
|
+
if (stagedVersion !== target) {
|
|
789
|
+
throw new Error(
|
|
790
|
+
`staged closure version mismatch: expected ${target}, got ${stagedVersion ?? "unknown"}`
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
const dest = versionDir(rootDir, target);
|
|
794
|
+
if (fs32.existsSync(dest)) {
|
|
795
|
+
const aside = `${dest}.evicted-${this.now()}`;
|
|
796
|
+
await fs32.promises.rename(dest, aside);
|
|
797
|
+
await fs32.promises.rm(aside, { recursive: true, force: true }).catch(() => void 0);
|
|
798
|
+
}
|
|
799
|
+
await fs32.promises.rename(stagingDir, dest);
|
|
800
|
+
} catch (err) {
|
|
801
|
+
await fs32.promises.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
|
|
802
|
+
throw err;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
// ── Rollback ──────────────────────────────────────────────────────────
|
|
806
|
+
async rollbackServerUpdate() {
|
|
807
|
+
if (this.inFlight !== "idle") {
|
|
808
|
+
return {
|
|
809
|
+
accepted: false,
|
|
810
|
+
targetVersion: null,
|
|
811
|
+
restarting: false,
|
|
812
|
+
message: `Refused: another operation is in flight (${this.inFlight}).`
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
const state = this.readState();
|
|
816
|
+
if (state.pendingBoot !== null) {
|
|
817
|
+
return {
|
|
818
|
+
accepted: false,
|
|
819
|
+
targetVersion: null,
|
|
820
|
+
restarting: false,
|
|
821
|
+
message: `Refused: version ${state.pendingBoot.version} is already staged and awaiting restart.`
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
if (state.currentVersion === null || state.previousVersion === null) {
|
|
825
|
+
return {
|
|
826
|
+
accepted: false,
|
|
827
|
+
targetVersion: state.previousVersion,
|
|
828
|
+
restarting: false,
|
|
829
|
+
message: "Refused: no previous version available to roll back to."
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
|
|
833
|
+
const invalidReason = validateVersionDir(
|
|
834
|
+
this.rootDir(),
|
|
835
|
+
state.previousVersion,
|
|
836
|
+
nodeMajor,
|
|
837
|
+
this.spec
|
|
838
|
+
);
|
|
839
|
+
if (invalidReason !== null) {
|
|
840
|
+
return {
|
|
841
|
+
accepted: false,
|
|
842
|
+
targetVersion: state.previousVersion,
|
|
843
|
+
restarting: false,
|
|
844
|
+
message: `Refused: previous version ${state.previousVersion} is not loadable (${invalidReason}).`
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
writeServerRootState(this.rootDir(), {
|
|
848
|
+
...state,
|
|
849
|
+
pendingBoot: {
|
|
850
|
+
version: state.previousVersion,
|
|
851
|
+
fromVersion: state.currentVersion,
|
|
852
|
+
requestedAtMs: this.now(),
|
|
853
|
+
bootAttempts: 0
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
this.logger.info("root package rollback requested \u2014 restarting", {
|
|
857
|
+
meta: { fromVersion: state.currentVersion, toVersion: state.previousVersion }
|
|
858
|
+
});
|
|
859
|
+
this.restartServerFn(
|
|
860
|
+
`${RESTART_REASON_PREFIX}: rollback to ${this.spec.packageName}@${state.previousVersion}`
|
|
861
|
+
);
|
|
862
|
+
return {
|
|
863
|
+
accepted: true,
|
|
864
|
+
targetVersion: state.previousVersion,
|
|
865
|
+
restarting: true,
|
|
866
|
+
message: `Rolling back to ${this.spec.packageName}@${state.previousVersion} \u2014 restarting.`
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
// ── Plain restart ─────────────────────────────────────────────────────
|
|
870
|
+
/**
|
|
871
|
+
* Plain process restart — no version change. Backs the `restartServer` cap
|
|
872
|
+
* method. Guards mirror apply/rollback: refuse while a stage is in flight
|
|
873
|
+
* (a concurrent npm install must not be interrupted) or while a version is
|
|
874
|
+
* already staged awaiting restart (a naive bounce would boot the OLD version
|
|
875
|
+
* and orphan the pending one — the operator should apply/rollback instead).
|
|
876
|
+
*/
|
|
877
|
+
restartNode() {
|
|
878
|
+
const runningVersion = this.runningVersion();
|
|
879
|
+
if (this.inFlight !== "idle") {
|
|
880
|
+
return {
|
|
881
|
+
accepted: false,
|
|
882
|
+
targetVersion: runningVersion,
|
|
883
|
+
restarting: false,
|
|
884
|
+
message: `Refused: another operation is in flight (${this.inFlight}).`
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
const state = this.readState();
|
|
888
|
+
if (state.pendingBoot !== null) {
|
|
889
|
+
return {
|
|
890
|
+
accepted: false,
|
|
891
|
+
targetVersion: runningVersion,
|
|
892
|
+
restarting: false,
|
|
893
|
+
message: `Refused: version ${state.pendingBoot.version} is staged and awaiting restart \u2014 apply or roll it back instead of a plain restart.`
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
this.logger.info("plain node restart requested", { meta: { runningVersion } });
|
|
897
|
+
this.restartServerFn(`${RESTART_REASON_PREFIX}: manual restart`);
|
|
898
|
+
return {
|
|
899
|
+
accepted: true,
|
|
900
|
+
targetVersion: runningVersion,
|
|
901
|
+
restarting: true,
|
|
902
|
+
message: "Restarting the node \u2014 the supervisor relaunches it on the same version."
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
// ── Boot health confirmation ──────────────────────────────────────────
|
|
906
|
+
/**
|
|
907
|
+
* Called by the post-boot path once the node is READY (hub: post-boot
|
|
908
|
+
* pipeline; agent: first acked `$hub.registerNode`). When THIS process is
|
|
909
|
+
* the probation boot of a pending version, promote it: `currentVersion` ←
|
|
910
|
+
* pending, `previousVersion` ← the version it replaced (N-1 retained for
|
|
911
|
+
* rollback), clear `pendingBoot` + `rolledBack`, and prune every version
|
|
912
|
+
* dir beyond {current, previous}. No-op otherwise.
|
|
913
|
+
*/
|
|
914
|
+
confirmBootHealthy() {
|
|
915
|
+
const state = readServerRootState(this.rootDir());
|
|
916
|
+
if (state === null || state.pendingBoot === null) return { promoted: null };
|
|
917
|
+
const activeVersion = this.env[this.envNames.activeVersion] ?? null;
|
|
918
|
+
if (activeVersion !== state.pendingBoot.version) {
|
|
919
|
+
return { promoted: null };
|
|
920
|
+
}
|
|
921
|
+
const promoted = {
|
|
922
|
+
...state,
|
|
923
|
+
currentVersion: state.pendingBoot.version,
|
|
924
|
+
previousVersion: state.pendingBoot.fromVersion,
|
|
925
|
+
pendingBoot: null,
|
|
926
|
+
rolledBack: null
|
|
927
|
+
};
|
|
928
|
+
writeServerRootState(this.rootDir(), promoted);
|
|
929
|
+
this.pruneVersions(
|
|
930
|
+
[promoted.currentVersion, promoted.previousVersion].filter(
|
|
931
|
+
(v) => typeof v === "string"
|
|
932
|
+
)
|
|
933
|
+
);
|
|
934
|
+
this.logger.info("root package update confirmed healthy", {
|
|
935
|
+
meta: {
|
|
936
|
+
version: promoted.currentVersion,
|
|
937
|
+
previousVersion: promoted.previousVersion
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
return { promoted: promoted.currentVersion };
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Remove version dirs under `versions/` not in `keep`.
|
|
944
|
+
*
|
|
945
|
+
* Transient dot-entries (`.staging-*` / `.evicted-*`) are NOT pruned unless
|
|
946
|
+
* older than {@link RootUpdateService.STALE_TRANSIENT_MS}: the API starts
|
|
947
|
+
* serving BEFORE the post-boot `confirmBootHealthy()` runs, so a concurrent
|
|
948
|
+
* `applyServerUpdate` can be mid-npm-install into a fresh `.staging-*` dir
|
|
949
|
+
* when this prune fires — deleting it would corrupt that install. Fresh
|
|
950
|
+
* transient dirs are owned by their in-flight operation (which cleans up on
|
|
951
|
+
* failure); the staleness sweep only collects dirs orphaned by a crash.
|
|
952
|
+
*/
|
|
953
|
+
pruneVersions(keep) {
|
|
954
|
+
const vDir = versionsDir(this.rootDir());
|
|
955
|
+
let entries;
|
|
956
|
+
try {
|
|
957
|
+
entries = fs32.readdirSync(vDir);
|
|
958
|
+
} catch {
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
for (const entry of entries) {
|
|
962
|
+
if (keep.includes(entry)) continue;
|
|
963
|
+
const full = path42.join(vDir, entry);
|
|
964
|
+
if (entry.startsWith(".")) {
|
|
965
|
+
try {
|
|
966
|
+
const ageMs = this.now() - fs32.statSync(full).mtimeMs;
|
|
967
|
+
if (ageMs < _RootUpdateService.STALE_TRANSIENT_MS) continue;
|
|
968
|
+
} catch {
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
try {
|
|
973
|
+
fs32.rmSync(full, { recursive: true, force: true });
|
|
974
|
+
this.logger.debug("pruned server-root version dir", { meta: { entry } });
|
|
975
|
+
} catch (err) {
|
|
976
|
+
this.logger.warn("failed to prune server-root version dir", {
|
|
977
|
+
meta: { entry, error: err instanceof Error ? err.message : String(err) }
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
/** Transient (.staging- / .evicted-) dirs younger than this are never pruned. */
|
|
983
|
+
static STALE_TRANSIENT_MS = 24 * 60 * 60 * 1e3;
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
|
|
26
988
|
// src/cli.ts
|
|
27
989
|
var import_node_fs = require("fs");
|
|
28
990
|
var import_node_module = require("module");
|
|
29
991
|
var os3 = __toESM(require("os"));
|
|
30
|
-
var
|
|
992
|
+
var path8 = __toESM(require("path"));
|
|
31
993
|
|
|
32
994
|
// src/agent-bootstrap.ts
|
|
33
|
-
var
|
|
34
|
-
var
|
|
995
|
+
var fs7 = __toESM(require("fs"));
|
|
996
|
+
var path7 = __toESM(require("path"));
|
|
35
997
|
|
|
36
998
|
// src/agent-http.ts
|
|
37
999
|
var fs = __toESM(require("fs"));
|
|
@@ -411,27 +1373,110 @@ function loadAgentConfig(configPath, dataDirOverride) {
|
|
|
411
1373
|
};
|
|
412
1374
|
}
|
|
413
1375
|
|
|
1376
|
+
// src/agent-update-service.ts
|
|
1377
|
+
var fs3 = __toESM(require("fs"));
|
|
1378
|
+
var path3 = __toESM(require("path"));
|
|
1379
|
+
var import_node_root = __toESM(require_dist());
|
|
1380
|
+
var AGENT_RUNTIME_ADDON_ID = "agent-runtime";
|
|
1381
|
+
var AGENT_RESTART_GRACE_MS = 2e3;
|
|
1382
|
+
var AGENT_LOCAL_CONFIRM_GRACE_MS = 9e4;
|
|
1383
|
+
function armLocalConfirmFallback(onGrace, graceMs = AGENT_LOCAL_CONFIRM_GRACE_MS) {
|
|
1384
|
+
const timer = setTimeout(() => onGrace(), graceMs);
|
|
1385
|
+
const maybeUnref = timer.unref;
|
|
1386
|
+
if (typeof maybeUnref === "function") maybeUnref.call(timer);
|
|
1387
|
+
return () => clearTimeout(timer);
|
|
1388
|
+
}
|
|
1389
|
+
function resolveAgentPackageJsonPath(fromDir) {
|
|
1390
|
+
const candidates = [
|
|
1391
|
+
path3.resolve(fromDir, "..", "package.json"),
|
|
1392
|
+
path3.resolve(fromDir, "..", "..", "package.json")
|
|
1393
|
+
];
|
|
1394
|
+
for (const candidate of candidates) {
|
|
1395
|
+
try {
|
|
1396
|
+
const raw = JSON.parse(fs3.readFileSync(candidate, "utf-8"));
|
|
1397
|
+
if (raw.name === import_node_root.AGENT_ROOT_SPEC.packageName) return candidate;
|
|
1398
|
+
} catch {
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
return candidates[0] ?? path3.resolve(fromDir, "..", "package.json");
|
|
1402
|
+
}
|
|
1403
|
+
function scheduleAgentRestart(logger, requestedBy, dataDir) {
|
|
1404
|
+
if (dataDir !== void 0 && dataDir.length > 0) {
|
|
1405
|
+
try {
|
|
1406
|
+
(0, import_node_root.writeRestartIntentMarker)((0, import_node_root.serverRootDir)(dataDir), {
|
|
1407
|
+
requestedAtMs: Date.now(),
|
|
1408
|
+
reason: requestedBy
|
|
1409
|
+
});
|
|
1410
|
+
} catch (err) {
|
|
1411
|
+
logger.warn("failed to write restart-intent marker", {
|
|
1412
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
logger.warn("agent restart requested \u2014 exiting for supervisor relaunch", {
|
|
1417
|
+
meta: { requestedBy, graceMs: AGENT_RESTART_GRACE_MS }
|
|
1418
|
+
});
|
|
1419
|
+
const grace = setTimeout(() => {
|
|
1420
|
+
process.kill(process.pid, "SIGTERM");
|
|
1421
|
+
const hard = setTimeout(() => process.exit(0), 15e3);
|
|
1422
|
+
hard.unref();
|
|
1423
|
+
}, AGENT_RESTART_GRACE_MS);
|
|
1424
|
+
grace.unref();
|
|
1425
|
+
}
|
|
1426
|
+
var AgentUpdateService = class extends import_node_root.RootUpdateService {
|
|
1427
|
+
constructor(options) {
|
|
1428
|
+
super({
|
|
1429
|
+
spec: import_node_root.AGENT_ROOT_SPEC,
|
|
1430
|
+
envNames: {
|
|
1431
|
+
bootMode: "CAMSTACK_AGENT_BOOT_MODE",
|
|
1432
|
+
activeVersion: "CAMSTACK_AGENT_ACTIVE_VERSION",
|
|
1433
|
+
seedDir: "CAMSTACK_SEED_AGENT_DIR"
|
|
1434
|
+
},
|
|
1435
|
+
logger: options.logger,
|
|
1436
|
+
restartServer: options.restartAgent ?? ((requestedBy) => scheduleAgentRestart(options.logger, requestedBy, options.dataDir)),
|
|
1437
|
+
dataDir: options.dataDir,
|
|
1438
|
+
runningPackageJsonPath: options.runningPackageJsonPath ?? resolveAgentPackageJsonPath(__dirname),
|
|
1439
|
+
workspaceProbeDir: __dirname,
|
|
1440
|
+
execNpm: options.execNpm,
|
|
1441
|
+
env: options.env,
|
|
1442
|
+
now: options.now
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
};
|
|
1446
|
+
function buildAgentServerManagementProvider(service) {
|
|
1447
|
+
return {
|
|
1448
|
+
getServerPackageStatus: () => service.getServerPackageStatus(),
|
|
1449
|
+
checkServerUpdate: () => service.checkServerUpdate(),
|
|
1450
|
+
applyServerUpdate: (input) => service.applyServerUpdate(input),
|
|
1451
|
+
rollbackServerUpdate: () => service.rollbackServerUpdate(),
|
|
1452
|
+
restartServer: () => Promise.resolve(service.restartNode())
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
function agentRuntimeManifestEntry() {
|
|
1456
|
+
return { addonId: AGENT_RUNTIME_ADDON_ID, capabilities: ["server-management"] };
|
|
1457
|
+
}
|
|
1458
|
+
|
|
414
1459
|
// src/agent-service.ts
|
|
415
|
-
var
|
|
1460
|
+
var fs5 = __toESM(require("fs"));
|
|
416
1461
|
var os2 = __toESM(require("os"));
|
|
417
|
-
var
|
|
1462
|
+
var path5 = __toESM(require("path"));
|
|
418
1463
|
var import_system = require("@camstack/system");
|
|
419
1464
|
var import_moleculer = require("moleculer");
|
|
420
1465
|
|
|
421
1466
|
// src/agent-deploy-swap.ts
|
|
422
|
-
var
|
|
423
|
-
var
|
|
1467
|
+
var fs4 = __toESM(require("fs"));
|
|
1468
|
+
var path4 = __toESM(require("path"));
|
|
424
1469
|
var import_node_crypto = require("crypto");
|
|
425
1470
|
function rmrf(target) {
|
|
426
|
-
|
|
1471
|
+
fs4.rmSync(target, { recursive: true, force: true });
|
|
427
1472
|
}
|
|
428
1473
|
function moveDir(from, to) {
|
|
429
1474
|
try {
|
|
430
|
-
|
|
1475
|
+
fs4.renameSync(from, to);
|
|
431
1476
|
} catch (err) {
|
|
432
1477
|
const code = err.code;
|
|
433
1478
|
if (code === "EXDEV") {
|
|
434
|
-
|
|
1479
|
+
fs4.cpSync(from, to, { recursive: true });
|
|
435
1480
|
rmrf(from);
|
|
436
1481
|
} else {
|
|
437
1482
|
throw err;
|
|
@@ -440,32 +1485,32 @@ function moveDir(from, to) {
|
|
|
440
1485
|
}
|
|
441
1486
|
async function applyDeployedBundle(input) {
|
|
442
1487
|
const { addonsDir, addonId, bundle, extract, logger } = input;
|
|
443
|
-
|
|
444
|
-
const liveDir =
|
|
445
|
-
const liveParent =
|
|
446
|
-
const liveBase =
|
|
447
|
-
|
|
1488
|
+
fs4.mkdirSync(addonsDir, { recursive: true });
|
|
1489
|
+
const liveDir = path4.join(addonsDir, addonId);
|
|
1490
|
+
const liveParent = path4.dirname(liveDir);
|
|
1491
|
+
const liveBase = path4.basename(liveDir);
|
|
1492
|
+
fs4.mkdirSync(liveParent, { recursive: true });
|
|
448
1493
|
const token = (0, import_node_crypto.randomBytes)(6).toString("hex");
|
|
449
|
-
const nextDir =
|
|
450
|
-
const backupDir =
|
|
451
|
-
|
|
1494
|
+
const nextDir = path4.join(liveParent, `.${liveBase}.next.${token}`);
|
|
1495
|
+
const backupDir = path4.join(liveParent, `.${liveBase}.backup.${token}`);
|
|
1496
|
+
fs4.mkdirSync(nextDir, { recursive: true });
|
|
452
1497
|
try {
|
|
453
1498
|
await extract(bundle, nextDir);
|
|
454
1499
|
} catch (err) {
|
|
455
1500
|
rmrf(nextDir);
|
|
456
1501
|
throw err;
|
|
457
1502
|
}
|
|
458
|
-
const hadLive =
|
|
1503
|
+
const hadLive = fs4.existsSync(liveDir);
|
|
459
1504
|
if (hadLive) {
|
|
460
|
-
|
|
1505
|
+
fs4.renameSync(liveDir, backupDir);
|
|
461
1506
|
}
|
|
462
1507
|
try {
|
|
463
1508
|
moveDir(nextDir, liveDir);
|
|
464
1509
|
} catch (swapErr) {
|
|
465
1510
|
if (hadLive) {
|
|
466
1511
|
try {
|
|
467
|
-
if (
|
|
468
|
-
|
|
1512
|
+
if (fs4.existsSync(liveDir)) rmrf(liveDir);
|
|
1513
|
+
fs4.renameSync(backupDir, liveDir);
|
|
469
1514
|
} catch (restoreErr) {
|
|
470
1515
|
logger.error(
|
|
471
1516
|
`agent deploy swap: restore of "${addonId}" failed after a failed swap \u2014 manual recovery may be needed`,
|
|
@@ -540,8 +1585,8 @@ function getLocalIps() {
|
|
|
540
1585
|
}
|
|
541
1586
|
async function withTimeout(p, ms, fallback) {
|
|
542
1587
|
let timer;
|
|
543
|
-
const timeout = new Promise((
|
|
544
|
-
timer = setTimeout(() =>
|
|
1588
|
+
const timeout = new Promise((resolve7) => {
|
|
1589
|
+
timer = setTimeout(() => resolve7(fallback), ms);
|
|
545
1590
|
});
|
|
546
1591
|
try {
|
|
547
1592
|
return await Promise.race([p, timeout]);
|
|
@@ -575,8 +1620,8 @@ function resolveDeployAction(seam) {
|
|
|
575
1620
|
function readHubAddressFromConfig(configPath) {
|
|
576
1621
|
if (!configPath) return null;
|
|
577
1622
|
try {
|
|
578
|
-
if (!
|
|
579
|
-
const raw = JSON.parse(
|
|
1623
|
+
if (!fs5.existsSync(configPath)) return null;
|
|
1624
|
+
const raw = JSON.parse(fs5.readFileSync(configPath, "utf-8"));
|
|
580
1625
|
return typeof raw.hubAddress === "string" && raw.hubAddress.length > 0 ? raw.hubAddress : null;
|
|
581
1626
|
} catch {
|
|
582
1627
|
return null;
|
|
@@ -584,9 +1629,9 @@ function readHubAddressFromConfig(configPath) {
|
|
|
584
1629
|
}
|
|
585
1630
|
function readDeployedAddonIds(addonDir) {
|
|
586
1631
|
try {
|
|
587
|
-
const manifestPath =
|
|
588
|
-
if (!
|
|
589
|
-
const raw = JSON.parse(
|
|
1632
|
+
const manifestPath = path5.join(addonDir, "package.json");
|
|
1633
|
+
if (!fs5.existsSync(manifestPath)) return [];
|
|
1634
|
+
const raw = JSON.parse(fs5.readFileSync(manifestPath, "utf-8"));
|
|
590
1635
|
const entries = raw.camstack?.addons ?? [];
|
|
591
1636
|
const ids = [];
|
|
592
1637
|
for (const entry of entries) {
|
|
@@ -726,17 +1771,17 @@ function createAgentService(deps) {
|
|
|
726
1771
|
deps.agentName = newName.trim();
|
|
727
1772
|
broker.logger.info(`Agent renamed: "${oldName}" \u2192 "${deps.agentName}"`);
|
|
728
1773
|
try {
|
|
729
|
-
const configFile =
|
|
1774
|
+
const configFile = path5.resolve(deps.configPath);
|
|
730
1775
|
let raw = {};
|
|
731
|
-
if (
|
|
1776
|
+
if (fs5.existsSync(configFile)) {
|
|
732
1777
|
try {
|
|
733
|
-
raw = JSON.parse(
|
|
1778
|
+
raw = JSON.parse(fs5.readFileSync(configFile, "utf-8"));
|
|
734
1779
|
} catch {
|
|
735
1780
|
}
|
|
736
1781
|
}
|
|
737
1782
|
raw.name = deps.agentName;
|
|
738
|
-
|
|
739
|
-
|
|
1783
|
+
fs5.mkdirSync(path5.dirname(configFile), { recursive: true });
|
|
1784
|
+
fs5.writeFileSync(configFile, JSON.stringify(raw, null, 2), "utf-8");
|
|
740
1785
|
broker.logger.info(`Agent name persisted to ${configFile}`);
|
|
741
1786
|
} catch (err) {
|
|
742
1787
|
broker.logger.warn(
|
|
@@ -760,15 +1805,15 @@ function createAgentService(deps) {
|
|
|
760
1805
|
const { promisify } = await import("util");
|
|
761
1806
|
const execFileAsync = promisify(execFile);
|
|
762
1807
|
const extract = async (tgz, destDir) => {
|
|
763
|
-
const tgzPath =
|
|
764
|
-
|
|
1808
|
+
const tgzPath = path5.join(destDir, "..", `.${path5.basename(destDir)}.tgz`);
|
|
1809
|
+
fs5.writeFileSync(tgzPath, tgz);
|
|
765
1810
|
try {
|
|
766
1811
|
await execFileAsync("tar", ["-xzf", tgzPath, "-C", destDir, "--strip-components=1"], {
|
|
767
1812
|
timeout: 6e4
|
|
768
1813
|
});
|
|
769
1814
|
} finally {
|
|
770
1815
|
try {
|
|
771
|
-
|
|
1816
|
+
fs5.unlinkSync(tgzPath);
|
|
772
1817
|
} catch {
|
|
773
1818
|
}
|
|
774
1819
|
}
|
|
@@ -819,26 +1864,26 @@ function createAgentService(deps) {
|
|
|
819
1864
|
const { execFile } = await import("child_process");
|
|
820
1865
|
const { promisify } = await import("util");
|
|
821
1866
|
const execFileAsync = promisify(execFile);
|
|
822
|
-
const modelsDir =
|
|
1867
|
+
const modelsDir = path5.join(deps.dataDir, "models");
|
|
823
1868
|
const seam = {
|
|
824
1869
|
modelsDir,
|
|
825
1870
|
fetchBundle: (s) => fetchBundleFromHub(s),
|
|
826
1871
|
extract: async (tgz, destDir) => {
|
|
827
|
-
const tmp =
|
|
1872
|
+
const tmp = path5.join(
|
|
828
1873
|
destDir,
|
|
829
1874
|
`.dist-${Date.now()}-${Math.random().toString(36).slice(2)}.tgz`
|
|
830
1875
|
);
|
|
831
|
-
|
|
1876
|
+
fs5.writeFileSync(tmp, tgz);
|
|
832
1877
|
try {
|
|
833
1878
|
await execFileAsync("tar", ["-xzf", tmp, "-C", destDir], { timeout: 6e4 });
|
|
834
1879
|
} finally {
|
|
835
1880
|
try {
|
|
836
|
-
|
|
1881
|
+
fs5.unlinkSync(tmp);
|
|
837
1882
|
} catch {
|
|
838
1883
|
}
|
|
839
1884
|
}
|
|
840
1885
|
},
|
|
841
|
-
mkdirp: (dir) =>
|
|
1886
|
+
mkdirp: (dir) => fs5.mkdirSync(dir, { recursive: true })
|
|
842
1887
|
};
|
|
843
1888
|
return applyModelDistribution(seam, params);
|
|
844
1889
|
}
|
|
@@ -883,17 +1928,17 @@ function createAgentService(deps) {
|
|
|
883
1928
|
} catch {
|
|
884
1929
|
}
|
|
885
1930
|
deps.loadedAddons.delete(addonId);
|
|
886
|
-
const addonDir =
|
|
887
|
-
if (
|
|
888
|
-
|
|
1931
|
+
const addonDir = path5.join(deps.addonsDir, addonId);
|
|
1932
|
+
if (fs5.existsSync(addonDir)) {
|
|
1933
|
+
fs5.rmSync(addonDir, { recursive: true, force: true });
|
|
889
1934
|
}
|
|
890
1935
|
const pkgName = entry?.packageName;
|
|
891
1936
|
if (pkgName && pkgName !== addonId) {
|
|
892
1937
|
const pkgShared = [...deps.loadedAddons.values()].some((e) => e.packageName === pkgName);
|
|
893
1938
|
if (!pkgShared) {
|
|
894
|
-
const pkgDir =
|
|
895
|
-
if (
|
|
896
|
-
|
|
1939
|
+
const pkgDir = path5.join(deps.addonsDir, pkgName);
|
|
1940
|
+
if (fs5.existsSync(pkgDir)) {
|
|
1941
|
+
fs5.rmSync(pkgDir, { recursive: true, force: true });
|
|
897
1942
|
}
|
|
898
1943
|
}
|
|
899
1944
|
}
|
|
@@ -961,14 +2006,14 @@ function createAgentService(deps) {
|
|
|
961
2006
|
}
|
|
962
2007
|
|
|
963
2008
|
// src/agent-group-runner.ts
|
|
964
|
-
var
|
|
965
|
-
var
|
|
2009
|
+
var fs6 = __toESM(require("fs"));
|
|
2010
|
+
var path6 = __toESM(require("path"));
|
|
966
2011
|
var import_types = require("@camstack/types");
|
|
967
2012
|
function readPackageManifestAddons(dir) {
|
|
968
2013
|
try {
|
|
969
|
-
const pkgPath =
|
|
970
|
-
if (!
|
|
971
|
-
const raw = JSON.parse(
|
|
2014
|
+
const pkgPath = path6.join(dir, "package.json");
|
|
2015
|
+
if (!fs6.existsSync(pkgPath)) return null;
|
|
2016
|
+
const raw = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
|
|
972
2017
|
const packageName = typeof raw.name === "string" ? raw.name : "";
|
|
973
2018
|
const packageVersion = typeof raw.version === "string" ? raw.version : "0.0.0";
|
|
974
2019
|
const entries = Array.isArray(raw.camstack?.addons) ? raw.camstack.addons : [];
|
|
@@ -1139,7 +2184,7 @@ async function startAgent(configPath) {
|
|
|
1139
2184
|
const bundledDir = process.env["CAMSTACK_BUNDLED_ADDONS_DIR"];
|
|
1140
2185
|
let workspaceDir = null;
|
|
1141
2186
|
let resolvedSource = explicitSource;
|
|
1142
|
-
if (bundledDir &&
|
|
2187
|
+
if (bundledDir && fs7.existsSync(bundledDir)) {
|
|
1143
2188
|
workspaceDir = bundledDir;
|
|
1144
2189
|
resolvedSource = "local";
|
|
1145
2190
|
console.log(`[Agent] Using bundled addons from ${bundledDir}`);
|
|
@@ -1203,9 +2248,9 @@ async function startAgent(configPath) {
|
|
|
1203
2248
|
return discoveryMode ? "searching" : "disconnected";
|
|
1204
2249
|
}
|
|
1205
2250
|
function readConfigFile2(p) {
|
|
1206
|
-
if (!
|
|
2251
|
+
if (!fs7.existsSync(p)) return {};
|
|
1207
2252
|
try {
|
|
1208
|
-
return JSON.parse(
|
|
2253
|
+
return JSON.parse(fs7.readFileSync(p, "utf-8"));
|
|
1209
2254
|
} catch {
|
|
1210
2255
|
return {};
|
|
1211
2256
|
}
|
|
@@ -1235,6 +2280,7 @@ async function startAgent(configPath) {
|
|
|
1235
2280
|
const caps = addonCapMap.get(addonId) ?? [];
|
|
1236
2281
|
result.push({ addonId, capabilities: caps });
|
|
1237
2282
|
}
|
|
2283
|
+
result.push(agentRuntimeManifestEntry());
|
|
1238
2284
|
return result;
|
|
1239
2285
|
}
|
|
1240
2286
|
function aggregateManifest() {
|
|
@@ -1251,11 +2297,13 @@ async function startAgent(configPath) {
|
|
|
1251
2297
|
allNativeCaps.push(...childNativeCaps);
|
|
1252
2298
|
}
|
|
1253
2299
|
}
|
|
2300
|
+
const rootPackage = agentUpdateService.getRunningRootPackage();
|
|
1254
2301
|
return (0, import_system3.buildNodeManifest)(
|
|
1255
2302
|
config.nodeId,
|
|
1256
2303
|
allAddons,
|
|
1257
2304
|
allNativeCaps.length > 0 ? allNativeCaps : void 0,
|
|
1258
|
-
config.secret ? (0, import_system3.hashClusterSecret)(config.secret) : void 0
|
|
2305
|
+
config.secret ? (0, import_system3.hashClusterSecret)(config.secret) : void 0,
|
|
2306
|
+
rootPackage.version === null ? void 0 : { name: rootPackage.name, version: rootPackage.version }
|
|
1259
2307
|
);
|
|
1260
2308
|
}
|
|
1261
2309
|
function triggerUpwardRegistration() {
|
|
@@ -1267,7 +2315,9 @@ async function startAgent(configPath) {
|
|
|
1267
2315
|
signal: registerAbortController.signal,
|
|
1268
2316
|
log: (msg) => console.log(`[Agent] ${msg}`)
|
|
1269
2317
|
}
|
|
1270
|
-
).
|
|
2318
|
+
).then(() => {
|
|
2319
|
+
confirmAgentBootHealthy("hub-ack");
|
|
2320
|
+
}).catch((err) => {
|
|
1271
2321
|
if ((0, import_system3.isClusterSecretMismatchError)(err)) {
|
|
1272
2322
|
consoleLogger.error(
|
|
1273
2323
|
"hub registration rejected: cluster secret mismatch \u2014 correct CAMSTACK_CLUSTER_SECRET and restart the agent"
|
|
@@ -1297,11 +2347,41 @@ async function startAgent(configPath) {
|
|
|
1297
2347
|
import_types3.pipelineExecutorCapability,
|
|
1298
2348
|
import_types3.pipelineRunnerCapability,
|
|
1299
2349
|
import_types3.audioAnalyzerCapability,
|
|
1300
|
-
import_types3.platformProbeCapability
|
|
2350
|
+
import_types3.platformProbeCapability,
|
|
2351
|
+
import_types3.serverManagementCapability
|
|
1301
2352
|
];
|
|
1302
2353
|
for (const cap of agentCapabilities) {
|
|
1303
2354
|
capabilityRegistry.declareCapability(cap);
|
|
1304
2355
|
}
|
|
2356
|
+
const agentUpdateService = new AgentUpdateService({
|
|
2357
|
+
logger: consoleLogger,
|
|
2358
|
+
dataDir: config.dataDir
|
|
2359
|
+
});
|
|
2360
|
+
capabilityRegistry.registerProvider(
|
|
2361
|
+
"server-management",
|
|
2362
|
+
AGENT_RUNTIME_ADDON_ID,
|
|
2363
|
+
buildAgentServerManagementProvider(agentUpdateService)
|
|
2364
|
+
);
|
|
2365
|
+
let agentBootConfirmed = false;
|
|
2366
|
+
let cancelLocalConfirmFallback = null;
|
|
2367
|
+
const confirmAgentBootHealthy = (source) => {
|
|
2368
|
+
if (agentBootConfirmed) return;
|
|
2369
|
+
agentBootConfirmed = true;
|
|
2370
|
+
cancelLocalConfirmFallback?.();
|
|
2371
|
+
cancelLocalConfirmFallback = null;
|
|
2372
|
+
try {
|
|
2373
|
+
const confirm = agentUpdateService.confirmBootHealthy();
|
|
2374
|
+
if (confirm.promoted !== null) {
|
|
2375
|
+
consoleLogger.info(
|
|
2376
|
+
`agent root package update confirmed healthy (${source}): @camstack/agent@${confirm.promoted}`
|
|
2377
|
+
);
|
|
2378
|
+
}
|
|
2379
|
+
} catch (err) {
|
|
2380
|
+
consoleLogger.warn(
|
|
2381
|
+
`agent root boot confirmation failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2382
|
+
);
|
|
2383
|
+
}
|
|
2384
|
+
};
|
|
1305
2385
|
const loggerFactory = (addonId) => agentLogManager.createLogger().withTags({ addonId });
|
|
1306
2386
|
const agentServiceSchema = createAgentService({
|
|
1307
2387
|
addonsDir: config.addonsDir,
|
|
@@ -1515,6 +2595,7 @@ async function startAgent(configPath) {
|
|
|
1515
2595
|
capabilityRegistry
|
|
1516
2596
|
);
|
|
1517
2597
|
triggerUpwardRegistration();
|
|
2598
|
+
cancelLocalConfirmFallback = armLocalConfirmFallback(() => confirmAgentBootHealthy("local-grace"));
|
|
1518
2599
|
let hubReachableFired = false;
|
|
1519
2600
|
broker.localBus.on("$node.connected", (data) => {
|
|
1520
2601
|
const node = data.node;
|
|
@@ -1538,6 +2619,8 @@ async function startAgent(configPath) {
|
|
|
1538
2619
|
const shutdown = async () => {
|
|
1539
2620
|
console.log("[Agent] Shutting down...");
|
|
1540
2621
|
registerAbortController.abort();
|
|
2622
|
+
cancelLocalConfirmFallback?.();
|
|
2623
|
+
cancelLocalConfirmFallback = null;
|
|
1541
2624
|
udsEventBridgeDispose?.();
|
|
1542
2625
|
udsEventBridgeDispose = null;
|
|
1543
2626
|
for (const [, entry] of loadedAddons) {
|
|
@@ -1698,7 +2781,7 @@ async function loadClusterCapableAddons(broker, config, capabilityRegistry, load
|
|
|
1698
2781
|
}
|
|
1699
2782
|
}
|
|
1700
2783
|
async function loadDeployedAddons(broker, addonsDir, dataDir, loadedAddons, storageProvider, loggerFactory, capabilityRegistry) {
|
|
1701
|
-
if (!
|
|
2784
|
+
if (!fs7.existsSync(addonsDir)) return;
|
|
1702
2785
|
const packageDirs = resolveAddonPackageDirs(addonsDir);
|
|
1703
2786
|
const groupCandidates = [];
|
|
1704
2787
|
const inProcessDirs = /* @__PURE__ */ new Set();
|
|
@@ -1758,7 +2841,7 @@ async function loadDeployedAddons(broker, addonsDir, dataDir, loadedAddons, stor
|
|
|
1758
2841
|
}
|
|
1759
2842
|
}
|
|
1760
2843
|
async function loadInProcessDeployedAddons(broker, dirs, dataDir, loadedAddons, storageProvider, loggerFactory, capabilityRegistry) {
|
|
1761
|
-
const modelsDir =
|
|
2844
|
+
const modelsDir = path7.join(process.env["CAMSTACK_DATA"] ?? dataDir, "models");
|
|
1762
2845
|
const contextOptions = {
|
|
1763
2846
|
storageProvider,
|
|
1764
2847
|
addonConfig: { modelsDir },
|
|
@@ -1808,13 +2891,13 @@ async function loadInProcessDeployedAddons(broker, dirs, dataDir, loadedAddons,
|
|
|
1808
2891
|
}
|
|
1809
2892
|
function readAgentVersion() {
|
|
1810
2893
|
const candidates = [
|
|
1811
|
-
|
|
1812
|
-
|
|
2894
|
+
path7.resolve(__dirname, "..", "package.json"),
|
|
2895
|
+
path7.resolve(__dirname, "..", "..", "package.json")
|
|
1813
2896
|
];
|
|
1814
2897
|
for (const candidate of candidates) {
|
|
1815
2898
|
try {
|
|
1816
|
-
if (!
|
|
1817
|
-
const raw = JSON.parse(
|
|
2899
|
+
if (!fs7.existsSync(candidate)) continue;
|
|
2900
|
+
const raw = JSON.parse(fs7.readFileSync(candidate, "utf-8"));
|
|
1818
2901
|
if (raw.name === "@camstack/agent" && typeof raw.version === "string") return raw.version;
|
|
1819
2902
|
} catch {
|
|
1820
2903
|
}
|
|
@@ -1823,39 +2906,44 @@ function readAgentVersion() {
|
|
|
1823
2906
|
}
|
|
1824
2907
|
function isDir(p) {
|
|
1825
2908
|
try {
|
|
1826
|
-
return
|
|
2909
|
+
return fs7.statSync(p).isDirectory();
|
|
1827
2910
|
} catch {
|
|
1828
2911
|
return false;
|
|
1829
2912
|
}
|
|
1830
2913
|
}
|
|
1831
2914
|
function resolveAddonPackageDirs(addonsDir) {
|
|
1832
2915
|
const dirs = [];
|
|
1833
|
-
if (!
|
|
1834
|
-
for (const name of
|
|
1835
|
-
const full =
|
|
2916
|
+
if (!fs7.existsSync(addonsDir)) return dirs;
|
|
2917
|
+
for (const name of fs7.readdirSync(addonsDir)) {
|
|
2918
|
+
const full = path7.join(addonsDir, name);
|
|
1836
2919
|
if (name.startsWith("@") && isDir(full)) {
|
|
1837
|
-
for (const sub of
|
|
1838
|
-
const subFull =
|
|
1839
|
-
if (isDir(subFull) &&
|
|
2920
|
+
for (const sub of fs7.readdirSync(full)) {
|
|
2921
|
+
const subFull = path7.join(full, sub);
|
|
2922
|
+
if (isDir(subFull) && fs7.existsSync(path7.join(subFull, "package.json"))) {
|
|
1840
2923
|
dirs.push(subFull);
|
|
1841
2924
|
}
|
|
1842
2925
|
}
|
|
1843
|
-
} else if (isDir(full) &&
|
|
2926
|
+
} else if (isDir(full) && fs7.existsSync(path7.join(full, "package.json"))) {
|
|
1844
2927
|
dirs.push(full);
|
|
1845
2928
|
}
|
|
1846
2929
|
}
|
|
1847
2930
|
return dirs;
|
|
1848
2931
|
}
|
|
1849
2932
|
function buildAgentChildUdsManifest(nodeId, childId, caps) {
|
|
1850
|
-
const
|
|
2933
|
+
const capsByAddon = /* @__PURE__ */ new Map();
|
|
1851
2934
|
for (const cap of caps) {
|
|
1852
|
-
if (cap.deviceId
|
|
1853
|
-
|
|
1854
|
-
|
|
2935
|
+
if (cap.deviceId !== void 0) continue;
|
|
2936
|
+
const addonId = cap.addonId ?? childId;
|
|
2937
|
+
const set = capsByAddon.get(addonId) ?? /* @__PURE__ */ new Set();
|
|
2938
|
+
set.add(cap.capName);
|
|
2939
|
+
capsByAddon.set(addonId, set);
|
|
1855
2940
|
}
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
2941
|
+
if (capsByAddon.size === 0) {
|
|
2942
|
+
capsByAddon.set(childId, /* @__PURE__ */ new Set());
|
|
2943
|
+
}
|
|
2944
|
+
const addons = [...capsByAddon.entries()].map(
|
|
2945
|
+
([addonId, capNames]) => ({ addonId, capabilities: [...capNames] })
|
|
2946
|
+
);
|
|
1859
2947
|
return { nodeId, addons };
|
|
1860
2948
|
}
|
|
1861
2949
|
var scriptName = process.argv[1] ?? "";
|
|
@@ -1981,18 +3069,18 @@ if (args.statusPort && !process.env["CAMSTACK_STATUS_PORT"]) {
|
|
|
1981
3069
|
if (!process.env["CAMSTACK_FRAMEWORK_DIR"]) {
|
|
1982
3070
|
const scriptPath = process.argv[1];
|
|
1983
3071
|
if (scriptPath) {
|
|
1984
|
-
const frameworkDir =
|
|
1985
|
-
const frameworkModules =
|
|
1986
|
-
if ((0, import_node_fs.existsSync)(
|
|
3072
|
+
const frameworkDir = path8.resolve(path8.dirname(scriptPath), "..", "..");
|
|
3073
|
+
const frameworkModules = path8.join(frameworkDir, "node_modules");
|
|
3074
|
+
if ((0, import_node_fs.existsSync)(path8.join(frameworkModules, "@camstack", "shm-ring"))) {
|
|
1987
3075
|
process.env["CAMSTACK_FRAMEWORK_DIR"] = frameworkDir;
|
|
1988
3076
|
const existingNodePath = process.env["NODE_PATH"];
|
|
1989
|
-
process.env["NODE_PATH"] = existingNodePath ? `${frameworkModules}${
|
|
3077
|
+
process.env["NODE_PATH"] = existingNodePath ? `${frameworkModules}${path8.delimiter}${existingNodePath}` : frameworkModules;
|
|
1990
3078
|
import_node_module.Module._initPaths();
|
|
1991
3079
|
console.log(
|
|
1992
3080
|
`[Agent] Self-contained framework detected \u2014 CAMSTACK_FRAMEWORK_DIR=${frameworkDir}`
|
|
1993
3081
|
);
|
|
1994
3082
|
if (!process.env["CAMSTACK_BUNDLED_ADDONS_DIR"]) {
|
|
1995
|
-
const bundledAddons =
|
|
3083
|
+
const bundledAddons = path8.join(frameworkDir, "addons");
|
|
1996
3084
|
if ((0, import_node_fs.existsSync)(bundledAddons)) {
|
|
1997
3085
|
process.env["CAMSTACK_BUNDLED_ADDONS_DIR"] = bundledAddons;
|
|
1998
3086
|
}
|