@camstack/agent 1.1.33 → 1.1.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,998 @@
1
+ import { createRequire as __cr } from 'node:module'; const require = globalThis.require ?? __cr(import.meta.url);
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
9
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
10
+ }) : x)(function(x) {
11
+ if (typeof require !== "undefined") return require.apply(this, arguments);
12
+ throw Error('Dynamic require of "' + x + '" is not supported');
13
+ });
14
+ var __commonJS = (cb, mod) => function __require2() {
15
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
16
+ };
17
+ var __copyProps = (to, from, except, desc) => {
18
+ if (from && typeof from === "object" || typeof from === "function") {
19
+ for (let key of __getOwnPropNames(from))
20
+ if (!__hasOwnProp.call(to, key) && key !== except)
21
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
22
+ }
23
+ return to;
24
+ };
25
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
26
+ // If the importer is in node compatibility mode or this is not an ESM
27
+ // file that has been converted to a CommonJS file using a Babel-
28
+ // compatible transform (i.e. "__esModule" has not been set), then set
29
+ // "default" to the CommonJS "module.exports" for node compatibility.
30
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
31
+ mod
32
+ ));
33
+
34
+ // ../node-root/dist/index.js
35
+ var require_dist = __commonJS({
36
+ "../node-root/dist/index.js"(exports, module) {
37
+ "use strict";
38
+ var __create2 = Object.create;
39
+ var __defProp2 = Object.defineProperty;
40
+ var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
41
+ var __getOwnPropNames2 = Object.getOwnPropertyNames;
42
+ var __getProtoOf2 = Object.getPrototypeOf;
43
+ var __hasOwnProp2 = Object.prototype.hasOwnProperty;
44
+ var __export = (target, all) => {
45
+ for (var name in all)
46
+ __defProp2(target, name, { get: all[name], enumerable: true });
47
+ };
48
+ var __copyProps2 = (to, from, except, desc) => {
49
+ if (from && typeof from === "object" || typeof from === "function") {
50
+ for (let key of __getOwnPropNames2(from))
51
+ if (!__hasOwnProp2.call(to, key) && key !== except)
52
+ __defProp2(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc2(from, key)) || desc.enumerable });
53
+ }
54
+ return to;
55
+ };
56
+ var __toESM2 = (mod, isNodeMode, target) => (target = mod != null ? __create2(__getProtoOf2(mod)) : {}, __copyProps2(
57
+ // If the importer is in node compatibility mode or this is not an ESM
58
+ // file that has been converted to a CommonJS file using a Babel-
59
+ // compatible transform (i.e. "__esModule" has not been set), then set
60
+ // "default" to the CommonJS "module.exports" for node compatibility.
61
+ isNodeMode || !mod || !mod.__esModule ? __defProp2(target, "default", { value: mod, enumerable: true }) : target,
62
+ mod
63
+ ));
64
+ var __toCommonJS = (mod) => __copyProps2(__defProp2({}, "__esModule", { value: true }), mod);
65
+ var src_exports = {};
66
+ __export(src_exports, {
67
+ AGENT_ROOT_SPEC: () => AGENT_ROOT_SPEC,
68
+ HOST_EXTERNAL_SPECIFIERS: () => HOST_EXTERNAL_SPECIFIERS,
69
+ HUB_ROOT_SPEC: () => HUB_ROOT_SPEC,
70
+ RESTART_INTENT_FILE: () => RESTART_INTENT_FILE,
71
+ RootUpdateService: () => RootUpdateService,
72
+ SERVER_ROOT_DIRNAME: () => SERVER_ROOT_DIRNAME,
73
+ SERVER_ROOT_STATE_FILE: () => SERVER_ROOT_STATE_FILE,
74
+ clearRestartIntentMarker: () => clearRestartIntentMarker,
75
+ compareSemver: () => compareSemver,
76
+ detectWorkspaceRoot: () => detectWorkspaceRoot,
77
+ emptyServerRootState: () => emptyServerRootState,
78
+ isHostExternal: () => isHostExternal,
79
+ isServerRootState: () => isServerRootState,
80
+ minNodeMajorOf: () => minNodeMajorOf,
81
+ planBoot: () => planBoot,
82
+ readRestartIntentMarker: () => readRestartIntentMarker,
83
+ readServerRootState: () => readServerRootState,
84
+ registerActiveRootResolver: () => registerActiveRootResolver,
85
+ restartIntentMarkerPath: () => restartIntentMarkerPath,
86
+ rootEntryPath: () => rootEntryPath,
87
+ rootPackageDir: () => rootPackageDir,
88
+ runNodeStarter: () => runNodeStarter,
89
+ serverRootDir: () => serverRootDir,
90
+ stateFilePath: () => stateFilePath,
91
+ validateVersionDir: () => validateVersionDir,
92
+ versionDir: () => versionDir,
93
+ versionsDir: () => versionsDir,
94
+ writeRestartIntentMarker: () => writeRestartIntentMarker,
95
+ writeServerRootState: () => writeServerRootState
96
+ });
97
+ module.exports = __toCommonJS(src_exports);
98
+ var HUB_ROOT_SPEC = {
99
+ packageName: "@camstack/server",
100
+ entryRelPath: ["dist", "launcher.js"]
101
+ };
102
+ var AGENT_ROOT_SPEC = {
103
+ packageName: "@camstack/agent",
104
+ entryRelPath: ["dist", "cli.js"]
105
+ };
106
+ var fs = __toESM2(__require("fs"));
107
+ var path = __toESM2(__require("path"));
108
+ var SERVER_ROOT_DIRNAME = "server-root";
109
+ var SERVER_ROOT_STATE_FILE = "state.json";
110
+ var RESTART_INTENT_FILE = ".restart-intent";
111
+ function emptyServerRootState() {
112
+ return {
113
+ schemaVersion: 1,
114
+ currentVersion: null,
115
+ previousVersion: null,
116
+ pendingBoot: null,
117
+ rolledBack: null
118
+ };
119
+ }
120
+ function serverRootDir(dataDir) {
121
+ return path.join(dataDir, SERVER_ROOT_DIRNAME);
122
+ }
123
+ function versionsDir(rootDir) {
124
+ return path.join(rootDir, "versions");
125
+ }
126
+ function versionDir(rootDir, version) {
127
+ return path.join(versionsDir(rootDir), version);
128
+ }
129
+ function rootPackageDir(versionDirPath, spec) {
130
+ return path.join(versionDirPath, "node_modules", ...spec.packageName.split("/"));
131
+ }
132
+ function rootEntryPath(versionDirPath, spec) {
133
+ return path.join(rootPackageDir(versionDirPath, spec), ...spec.entryRelPath);
134
+ }
135
+ function stateFilePath(rootDir) {
136
+ return path.join(rootDir, SERVER_ROOT_STATE_FILE);
137
+ }
138
+ function isNullableString(v) {
139
+ return v === null || typeof v === "string";
140
+ }
141
+ function isPendingBoot(v) {
142
+ if (typeof v !== "object" || v === null) return false;
143
+ const p = v;
144
+ return typeof p["version"] === "string" && isNullableString(p["fromVersion"]) && typeof p["requestedAtMs"] === "number" && typeof p["bootAttempts"] === "number";
145
+ }
146
+ function isRollbackInfo(v) {
147
+ if (typeof v !== "object" || v === null) return false;
148
+ const r = v;
149
+ return typeof r["fromVersion"] === "string" && isNullableString(r["toVersion"]) && typeof r["atMs"] === "number" && typeof r["reason"] === "string";
150
+ }
151
+ function isServerRootState(v) {
152
+ if (typeof v !== "object" || v === null) return false;
153
+ const s = v;
154
+ if (s["schemaVersion"] !== 1) return false;
155
+ if (!isNullableString(s["currentVersion"])) return false;
156
+ if (!isNullableString(s["previousVersion"])) return false;
157
+ if (s["pendingBoot"] !== null && !isPendingBoot(s["pendingBoot"])) return false;
158
+ if (s["rolledBack"] !== null && !isRollbackInfo(s["rolledBack"])) return false;
159
+ return true;
160
+ }
161
+ function readServerRootState(rootDir) {
162
+ try {
163
+ const raw = JSON.parse(fs.readFileSync(stateFilePath(rootDir), "utf-8"));
164
+ return isServerRootState(raw) ? raw : null;
165
+ } catch {
166
+ return null;
167
+ }
168
+ }
169
+ function writeServerRootState(rootDir, state) {
170
+ fs.mkdirSync(rootDir, { recursive: true });
171
+ const target = stateFilePath(rootDir);
172
+ const tmp = `${target}.tmp`;
173
+ fs.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf-8");
174
+ fs.renameSync(tmp, target);
175
+ }
176
+ function restartIntentMarkerPath(rootDir) {
177
+ return path.join(rootDir, RESTART_INTENT_FILE);
178
+ }
179
+ function isRestartIntentMarker(v) {
180
+ if (typeof v !== "object" || v === null) return false;
181
+ const m = v;
182
+ return typeof m["requestedAtMs"] === "number" && typeof m["reason"] === "string";
183
+ }
184
+ function writeRestartIntentMarker(rootDir, marker) {
185
+ fs.mkdirSync(rootDir, { recursive: true });
186
+ const target = restartIntentMarkerPath(rootDir);
187
+ const tmp = `${target}.tmp`;
188
+ fs.writeFileSync(tmp, JSON.stringify(marker, null, 2), "utf-8");
189
+ fs.renameSync(tmp, target);
190
+ }
191
+ function readRestartIntentMarker(rootDir) {
192
+ try {
193
+ const raw = JSON.parse(fs.readFileSync(restartIntentMarkerPath(rootDir), "utf-8"));
194
+ return isRestartIntentMarker(raw) ? raw : null;
195
+ } catch {
196
+ return null;
197
+ }
198
+ }
199
+ function clearRestartIntentMarker(rootDir) {
200
+ try {
201
+ fs.rmSync(restartIntentMarkerPath(rootDir), { force: true });
202
+ } catch {
203
+ }
204
+ }
205
+ function minNodeMajorOf(enginesNode) {
206
+ const match = /(\d+)/.exec(enginesNode);
207
+ if (match === null) return null;
208
+ const major = Number.parseInt(match[1] ?? "", 10);
209
+ return Number.isNaN(major) ? null : major;
210
+ }
211
+ function validateVersionDir(rootDir, version, nodeMajor, spec) {
212
+ const vDir = versionDir(rootDir, version);
213
+ const entry = rootEntryPath(vDir, spec);
214
+ if (!fs.existsSync(entry)) {
215
+ return `root entry missing: ${entry}`;
216
+ }
217
+ const pkgJsonPath = path.join(rootPackageDir(vDir, spec), "package.json");
218
+ let pkg;
219
+ try {
220
+ const raw = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
221
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
222
+ return `package.json malformed: ${pkgJsonPath}`;
223
+ }
224
+ pkg = raw;
225
+ } catch {
226
+ return `package.json unreadable: ${pkgJsonPath}`;
227
+ }
228
+ if (pkg["name"] !== spec.packageName) {
229
+ return `package name mismatch: expected ${spec.packageName}, got ${String(pkg["name"])}`;
230
+ }
231
+ if (pkg["version"] !== version) {
232
+ return `package version mismatch: dir says ${version}, package.json says ${String(pkg["version"])}`;
233
+ }
234
+ const engines = pkg["engines"];
235
+ if (typeof engines === "object" && engines !== null) {
236
+ const enginesNode = engines["node"];
237
+ if (typeof enginesNode === "string") {
238
+ const minMajor = minNodeMajorOf(enginesNode);
239
+ if (minMajor !== null && nodeMajor < minMajor) {
240
+ return `engines.node "${enginesNode}" requires Node >= ${minMajor}, runtime is ${nodeMajor}`;
241
+ }
242
+ }
243
+ }
244
+ return null;
245
+ }
246
+ function planBoot(state, isValidVersion, now) {
247
+ if (state === null) {
248
+ return { kind: "baked", reason: "no server-root state", stateToWrite: null };
249
+ }
250
+ let next = state;
251
+ let changed = false;
252
+ if (next.pendingBoot !== null) {
253
+ const pending = next.pendingBoot;
254
+ if (pending.bootAttempts >= 1) {
255
+ next = {
256
+ ...next,
257
+ pendingBoot: null,
258
+ rolledBack: {
259
+ fromVersion: pending.version,
260
+ toVersion: next.currentVersion,
261
+ atMs: now(),
262
+ reason: "probation boot did not reach ready \u2014 rolled back"
263
+ }
264
+ };
265
+ changed = true;
266
+ } else if (!isValidVersion(pending.version)) {
267
+ next = {
268
+ ...next,
269
+ pendingBoot: null,
270
+ rolledBack: {
271
+ fromVersion: pending.version,
272
+ toVersion: next.currentVersion,
273
+ atMs: now(),
274
+ reason: "staged version failed validation"
275
+ }
276
+ };
277
+ changed = true;
278
+ } else {
279
+ return {
280
+ kind: "data-root",
281
+ version: pending.version,
282
+ probation: true,
283
+ stateToWrite: {
284
+ ...next,
285
+ pendingBoot: { ...pending, bootAttempts: pending.bootAttempts + 1 }
286
+ }
287
+ };
288
+ }
289
+ }
290
+ if (next.currentVersion !== null) {
291
+ if (isValidVersion(next.currentVersion)) {
292
+ return {
293
+ kind: "data-root",
294
+ version: next.currentVersion,
295
+ probation: false,
296
+ stateToWrite: changed ? next : null
297
+ };
298
+ }
299
+ const broken = next.currentVersion;
300
+ if (next.previousVersion !== null && isValidVersion(next.previousVersion)) {
301
+ const fallback = next.previousVersion;
302
+ return {
303
+ kind: "data-root",
304
+ version: fallback,
305
+ probation: false,
306
+ stateToWrite: {
307
+ ...next,
308
+ currentVersion: fallback,
309
+ previousVersion: null,
310
+ rolledBack: {
311
+ fromVersion: broken,
312
+ toVersion: fallback,
313
+ atMs: now(),
314
+ reason: "active version failed validation"
315
+ }
316
+ }
317
+ };
318
+ }
319
+ return {
320
+ kind: "baked",
321
+ reason: `active version ${broken} failed validation and no valid previous version exists`,
322
+ stateToWrite: {
323
+ ...next,
324
+ currentVersion: null,
325
+ rolledBack: {
326
+ fromVersion: broken,
327
+ toVersion: null,
328
+ atMs: now(),
329
+ reason: "active version failed validation"
330
+ }
331
+ }
332
+ };
333
+ }
334
+ return {
335
+ kind: "baked",
336
+ reason: "no active data-root version",
337
+ stateToWrite: changed ? next : null
338
+ };
339
+ }
340
+ function parse(version) {
341
+ const dashIdx = version.indexOf("-");
342
+ const base = dashIdx === -1 ? version : version.slice(0, dashIdx);
343
+ const prerelease = dashIdx === -1 ? null : version.slice(dashIdx + 1);
344
+ const nums = base.split(".").map((seg) => {
345
+ const n = Number.parseInt(seg, 10);
346
+ return Number.isNaN(n) ? 0 : n;
347
+ });
348
+ return { nums, prerelease };
349
+ }
350
+ function compareSemver(a, b) {
351
+ const pa = parse(a);
352
+ const pb = parse(b);
353
+ const len = Math.max(pa.nums.length, pb.nums.length);
354
+ for (let i = 0; i < len; i++) {
355
+ const na = pa.nums[i] ?? 0;
356
+ const nb = pb.nums[i] ?? 0;
357
+ if (na < nb) return -1;
358
+ if (na > nb) return 1;
359
+ }
360
+ if (pa.prerelease === null && pb.prerelease === null) return 0;
361
+ if (pa.prerelease === null) return 1;
362
+ if (pb.prerelease === null) return -1;
363
+ if (pa.prerelease < pb.prerelease) return -1;
364
+ if (pa.prerelease > pb.prerelease) return 1;
365
+ return 0;
366
+ }
367
+ var fs2 = __toESM2(__require("fs"));
368
+ var path2 = __toESM2(__require("path"));
369
+ function detectWorkspaceRoot(fromDir) {
370
+ let dir = path2.resolve(fromDir);
371
+ for (; ; ) {
372
+ const pkgPath = path2.join(dir, "package.json");
373
+ try {
374
+ const raw = JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
375
+ if (typeof raw === "object" && raw !== null && raw["workspaces"] !== void 0) {
376
+ return dir;
377
+ }
378
+ } catch {
379
+ }
380
+ const parent = path2.dirname(dir);
381
+ if (parent === dir) return null;
382
+ dir = parent;
383
+ }
384
+ }
385
+ var path3 = __toESM2(__require("path"));
386
+ var import_node_url = __require("url");
387
+ var HOST_EXTERNAL_SPECIFIERS = [
388
+ "@camstack/system",
389
+ "@camstack/shm-ring",
390
+ "@camstack/types",
391
+ "@camstack/sdk",
392
+ "zod",
393
+ "@trpc/server",
394
+ "@trpc/client",
395
+ "sharp",
396
+ "node-av"
397
+ ];
398
+ function isHostExternal(specifier) {
399
+ return HOST_EXTERNAL_SPECIFIERS.some(
400
+ (name) => specifier === name || specifier.startsWith(`${name}/`)
401
+ );
402
+ }
403
+ function registerActiveRootResolver(activeRootDir) {
404
+ const moduleApi = __require("module");
405
+ const registerHooks = moduleApi["registerHooks"];
406
+ if (typeof registerHooks !== "function") {
407
+ console.warn(
408
+ "[starter] module.registerHooks unavailable (Node < 22.15) \u2014 host-external redirect skipped"
409
+ );
410
+ return;
411
+ }
412
+ const anchorURL = (0, import_node_url.pathToFileURL)(
413
+ path3.join(activeRootDir, "node_modules", "__camstack_starter_anchor__.js")
414
+ ).href;
415
+ const hooks = {
416
+ resolve: (specifier, context, nextResolve) => {
417
+ if (isHostExternal(specifier)) {
418
+ try {
419
+ return nextResolve(specifier, { ...context, parentURL: anchorURL });
420
+ } catch {
421
+ }
422
+ }
423
+ return nextResolve(specifier, context);
424
+ }
425
+ };
426
+ registerHooks(hooks);
427
+ }
428
+ function runNodeStarter(options) {
429
+ const env = options.env ?? process.env;
430
+ const now = options.now ?? Date.now;
431
+ const registerResolver = options.registerResolver ?? registerActiveRootResolver;
432
+ const disabled = env[options.envNames.killSwitch] === "off";
433
+ const workspaceRoot = detectWorkspaceRoot(options.starterDir);
434
+ if (workspaceRoot !== null) {
435
+ console.log(
436
+ `[starter] workspace checkout detected at ${workspaceRoot} \u2014 using plain resolution`
437
+ );
438
+ env[options.envNames.bootMode] = "workspace";
439
+ options.loadEntry(options.seedEntry);
440
+ return;
441
+ }
442
+ if (disabled) {
443
+ console.log(`[starter] ${options.envNames.killSwitch}=off \u2014 booting the baked seed closure`);
444
+ env[options.envNames.bootMode] = "baked";
445
+ options.loadEntry(options.seedEntry);
446
+ return;
447
+ }
448
+ const rootDir = serverRootDir(options.dataDir);
449
+ const state = readServerRootState(rootDir);
450
+ const nodeMajor = options.nodeMajor ?? Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
451
+ const isValidVersion = (version) => {
452
+ const reason = validateVersionDir(rootDir, version, nodeMajor, options.spec);
453
+ if (reason !== null) console.warn(`[starter] version ${version} invalid: ${reason}`);
454
+ return reason === null;
455
+ };
456
+ const plan = planBoot(state, isValidVersion, now);
457
+ if (plan.stateToWrite !== null) {
458
+ try {
459
+ writeServerRootState(rootDir, plan.stateToWrite);
460
+ } catch (err) {
461
+ console.error("[starter] FAILED to persist server-root state \u2014 booting baked seed:", err);
462
+ env[options.envNames.bootMode] = "baked";
463
+ options.loadEntry(options.seedEntry);
464
+ return;
465
+ }
466
+ if (plan.stateToWrite.rolledBack !== null && (state?.rolledBack ?? null) !== plan.stateToWrite.rolledBack) {
467
+ const rb = plan.stateToWrite.rolledBack;
468
+ console.warn(
469
+ `[starter] ROLLED BACK ${rb.fromVersion} -> ${rb.toVersion ?? "baked seed"}: ${rb.reason}`
470
+ );
471
+ }
472
+ }
473
+ if (plan.kind === "data-root") {
474
+ const activeRootDir = versionDir(rootDir, plan.version);
475
+ const entry = rootEntryPath(activeRootDir, options.spec);
476
+ console.log(
477
+ `[starter] booting ${options.spec.packageName}@${plan.version} from ${activeRootDir}` + (plan.probation ? " (probation boot \u2014 health-check armed)" : "")
478
+ );
479
+ env[options.envNames.bootMode] = "data-root";
480
+ env[options.envNames.activeRoot] = activeRootDir;
481
+ env[options.envNames.activeVersion] = plan.version;
482
+ registerResolver(activeRootDir);
483
+ options.loadEntry(entry);
484
+ return;
485
+ }
486
+ console.log(`[starter] booting baked seed closure (${plan.reason})`);
487
+ env[options.envNames.bootMode] = "baked";
488
+ options.loadEntry(options.seedEntry);
489
+ }
490
+ var import_node_child_process = __require("child_process");
491
+ var fs3 = __toESM2(__require("fs"));
492
+ var path4 = __toESM2(__require("path"));
493
+ var import_node_util = __require("util");
494
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
495
+ function buildNpmRegistryArgs(registry) {
496
+ if (registry === void 0 || registry.length === 0) return [];
497
+ return ["--registry", registry, `--@camstack:registry=${registry}`];
498
+ }
499
+ var NPM_VIEW_TIMEOUT_MS = 2e4;
500
+ var NPM_INSTALL_TIMEOUT_MS = 15 * 6e4;
501
+ var RESTART_REASON_PREFIX = "server-update";
502
+ function readPackageVersion(pkgJsonPath) {
503
+ try {
504
+ const raw = JSON.parse(fs3.readFileSync(pkgJsonPath, "utf-8"));
505
+ if (typeof raw === "object" && raw !== null) {
506
+ const version = raw["version"];
507
+ if (typeof version === "string") return version;
508
+ }
509
+ } catch {
510
+ }
511
+ return null;
512
+ }
513
+ var RootUpdateService = class _RootUpdateService {
514
+ spec;
515
+ envNames;
516
+ logger;
517
+ restartServerFn;
518
+ dataDir;
519
+ execNpm;
520
+ env;
521
+ now;
522
+ runningPackageJsonPath;
523
+ workspaceProbeDir;
524
+ checkCache = null;
525
+ inFlight = "idle";
526
+ constructor(options) {
527
+ this.spec = options.spec;
528
+ this.envNames = options.envNames;
529
+ this.logger = options.logger;
530
+ this.restartServerFn = options.restartServer;
531
+ this.dataDir = path4.resolve(options.dataDir);
532
+ this.execNpm = options.execNpm ?? (async (args, opts) => {
533
+ const { stdout } = await execFileAsync("npm", [...args], {
534
+ cwd: opts.cwd,
535
+ timeout: opts.timeoutMs
536
+ });
537
+ return { stdout };
538
+ });
539
+ this.env = options.env ?? process.env;
540
+ this.now = options.now ?? Date.now;
541
+ this.runningPackageJsonPath = options.runningPackageJsonPath;
542
+ this.workspaceProbeDir = options.workspaceProbeDir;
543
+ }
544
+ // ── Status ────────────────────────────────────────────────────────────
545
+ resolveBootMode() {
546
+ const raw = this.env[this.envNames.bootMode];
547
+ if (raw === "workspace" || raw === "baked" || raw === "data-root") return raw;
548
+ return detectWorkspaceRoot(this.workspaceProbeDir) !== null ? "workspace" : "baked";
549
+ }
550
+ runningVersion() {
551
+ return readPackageVersion(this.runningPackageJsonPath);
552
+ }
553
+ /** The running root package's identity — feeds registerNode / topology rows. */
554
+ getRunningRootPackage() {
555
+ return { name: this.spec.packageName, version: this.runningVersion() };
556
+ }
557
+ seedVersion() {
558
+ const seedDir = this.env[this.envNames.seedDir];
559
+ if (seedDir === void 0 || seedDir.length === 0) return null;
560
+ return readPackageVersion(path4.join(seedDir, "package.json"));
561
+ }
562
+ rootDir() {
563
+ return serverRootDir(this.dataDir);
564
+ }
565
+ readState() {
566
+ return readServerRootState(this.rootDir()) ?? emptyServerRootState();
567
+ }
568
+ /**
569
+ * Like `readState`, but distinguishes "state.json missing" (normal on a
570
+ * fresh node) from "state.json present but corrupt" — in the corrupt case
571
+ * the starter booted the baked seed while installed data-dir versions are
572
+ * silently ignored, which the status surface must make visible.
573
+ */
574
+ readStateInfo() {
575
+ const rootDir = this.rootDir();
576
+ const raw = readServerRootState(rootDir);
577
+ if (raw !== null) return { state: raw, corrupt: false };
578
+ return { state: emptyServerRootState(), corrupt: fs3.existsSync(stateFilePath(rootDir)) };
579
+ }
580
+ updateState(state) {
581
+ if (this.inFlight === "checking") return "checking";
582
+ if (this.inFlight === "staging") return "staging";
583
+ if (state.pendingBoot !== null) {
584
+ const activeVersion = this.env[this.envNames.activeVersion] ?? null;
585
+ if (activeVersion !== null && activeVersion === state.pendingBoot.version) {
586
+ return "awaiting-confirmation";
587
+ }
588
+ return "pending-restart";
589
+ }
590
+ return "idle";
591
+ }
592
+ async getServerPackageStatus() {
593
+ const { state, corrupt } = this.readStateInfo();
594
+ const runningVersion = this.runningVersion();
595
+ const latestVersion = this.checkCache?.latestVersion ?? null;
596
+ const updateAvailable = latestVersion !== null && runningVersion !== null && compareSemver(latestVersion, runningVersion) > 0;
597
+ return {
598
+ packageName: this.spec.packageName,
599
+ runningVersion,
600
+ nodeRuntimeVersion: process.versions.node,
601
+ activeVersion: this.env[this.envNames.activeVersion] ?? state.currentVersion,
602
+ previousVersion: state.previousVersion,
603
+ seedVersion: this.seedVersion(),
604
+ latestVersion,
605
+ updateAvailable,
606
+ bootMode: this.resolveBootMode(),
607
+ updateState: this.updateState(state),
608
+ pendingVersion: state.pendingBoot?.version ?? null,
609
+ rolledBack: state.rolledBack,
610
+ stateFileCorrupt: corrupt,
611
+ lastCheckedAtMs: this.checkCache?.checkedAtMs ?? null
612
+ };
613
+ }
614
+ // ── Check ─────────────────────────────────────────────────────────────
615
+ async checkServerUpdate() {
616
+ const runningVersion = this.runningVersion();
617
+ if (this.inFlight !== "idle") {
618
+ return {
619
+ packageName: this.spec.packageName,
620
+ runningVersion,
621
+ latestVersion: this.checkCache?.latestVersion ?? null,
622
+ updateAvailable: false,
623
+ checkedAtMs: this.checkCache?.checkedAtMs ?? this.now(),
624
+ error: `busy: ${this.inFlight}`
625
+ };
626
+ }
627
+ this.inFlight = "checking";
628
+ try {
629
+ const registry = this.env["CAMSTACK_NPM_REGISTRY"];
630
+ const args = [
631
+ "view",
632
+ `${this.spec.packageName}@latest`,
633
+ "version",
634
+ ...buildNpmRegistryArgs(registry)
635
+ ];
636
+ const { stdout } = await this.execNpm(args, { timeoutMs: NPM_VIEW_TIMEOUT_MS });
637
+ const latestVersion = stdout.trim().length > 0 ? stdout.trim() : null;
638
+ this.checkCache = { latestVersion, checkedAtMs: this.now(), error: null };
639
+ const updateAvailable = latestVersion !== null && runningVersion !== null && compareSemver(latestVersion, runningVersion) > 0;
640
+ return {
641
+ packageName: this.spec.packageName,
642
+ runningVersion,
643
+ latestVersion,
644
+ updateAvailable,
645
+ checkedAtMs: this.checkCache.checkedAtMs,
646
+ error: null
647
+ };
648
+ } catch (err) {
649
+ const message = err instanceof Error ? err.message : String(err);
650
+ this.checkCache = {
651
+ latestVersion: this.checkCache?.latestVersion ?? null,
652
+ checkedAtMs: this.now(),
653
+ error: message
654
+ };
655
+ this.logger.warn("root package update check failed", { meta: { error: message } });
656
+ return {
657
+ packageName: this.spec.packageName,
658
+ runningVersion,
659
+ latestVersion: this.checkCache.latestVersion,
660
+ updateAvailable: false,
661
+ checkedAtMs: this.checkCache.checkedAtMs,
662
+ error: message
663
+ };
664
+ } finally {
665
+ this.inFlight = "idle";
666
+ }
667
+ }
668
+ // ── Apply ─────────────────────────────────────────────────────────────
669
+ async applyServerUpdate(input) {
670
+ const bootMode = this.resolveBootMode();
671
+ if (bootMode === "workspace") {
672
+ return {
673
+ accepted: false,
674
+ targetVersion: input.version ?? null,
675
+ restarting: false,
676
+ message: "Refused: running from a workspace checkout \u2014 the dev loop resolves code from the workspace, a data-root update would never be loaded."
677
+ };
678
+ }
679
+ if (this.inFlight !== "idle") {
680
+ return {
681
+ accepted: false,
682
+ targetVersion: input.version ?? null,
683
+ restarting: false,
684
+ message: `Refused: another operation is in flight (${this.inFlight}).`
685
+ };
686
+ }
687
+ const stateBefore = this.readState();
688
+ if (stateBefore.pendingBoot !== null) {
689
+ return {
690
+ accepted: false,
691
+ targetVersion: input.version ?? null,
692
+ restarting: false,
693
+ message: `Refused: version ${stateBefore.pendingBoot.version} is already staged and awaiting restart.`
694
+ };
695
+ }
696
+ let target = input.version ?? null;
697
+ if (target === null) {
698
+ const check = await this.checkServerUpdate();
699
+ if (check.error !== null || check.latestVersion === null) {
700
+ return {
701
+ accepted: false,
702
+ targetVersion: null,
703
+ restarting: false,
704
+ message: `Refused: could not resolve the latest version (${check.error ?? "no version returned"}).`
705
+ };
706
+ }
707
+ target = check.latestVersion;
708
+ }
709
+ const runningVersion = this.runningVersion();
710
+ if (target === runningVersion) {
711
+ return {
712
+ accepted: false,
713
+ targetVersion: target,
714
+ restarting: false,
715
+ message: `Refused: ${this.spec.packageName}@${target} is already running.`
716
+ };
717
+ }
718
+ this.inFlight = "staging";
719
+ try {
720
+ await this.stageAndActivate(target);
721
+ } catch (err) {
722
+ const message = err instanceof Error ? err.message : String(err);
723
+ this.logger.error("root package update staging failed", {
724
+ meta: { targetVersion: target, error: message }
725
+ });
726
+ return {
727
+ accepted: false,
728
+ targetVersion: target,
729
+ restarting: false,
730
+ message: `Staging failed: ${message}`
731
+ };
732
+ } finally {
733
+ this.inFlight = "idle";
734
+ }
735
+ const state = this.readState();
736
+ writeServerRootState(this.rootDir(), {
737
+ ...state,
738
+ pendingBoot: {
739
+ version: target,
740
+ fromVersion: state.currentVersion,
741
+ requestedAtMs: this.now(),
742
+ bootAttempts: 0
743
+ },
744
+ rolledBack: null
745
+ });
746
+ this.logger.info("root package update staged \u2014 restarting to apply", {
747
+ meta: { targetVersion: target, fromVersion: state.currentVersion }
748
+ });
749
+ this.restartServerFn(`${RESTART_REASON_PREFIX}: ${this.spec.packageName}@${target}`);
750
+ return {
751
+ accepted: true,
752
+ targetVersion: target,
753
+ restarting: true,
754
+ message: `Staged ${this.spec.packageName}@${target} \u2014 restarting to apply (probation boot with auto-rollback).`
755
+ };
756
+ }
757
+ /**
758
+ * npm-install the target closure into a SAME-FS staging dir inside
759
+ * `versions/`, validate it, then atomically rename it into place.
760
+ */
761
+ async stageAndActivate(target) {
762
+ const rootDir = this.rootDir();
763
+ const vDir = versionsDir(rootDir);
764
+ fs3.mkdirSync(vDir, { recursive: true });
765
+ const stagingDir = path4.join(vDir, `.staging-${target}-${process.pid}-${this.now()}`);
766
+ fs3.mkdirSync(stagingDir, { recursive: true });
767
+ try {
768
+ fs3.writeFileSync(
769
+ path4.join(stagingDir, "package.json"),
770
+ JSON.stringify({ name: "camstack-node-root", private: true }, null, 2),
771
+ "utf-8"
772
+ );
773
+ const registry = this.env["CAMSTACK_NPM_REGISTRY"];
774
+ await this.execNpm(
775
+ [
776
+ "install",
777
+ "--omit=dev",
778
+ "--no-audit",
779
+ "--no-fund",
780
+ "--loglevel=error",
781
+ `${this.spec.packageName}@${target}`,
782
+ ...buildNpmRegistryArgs(registry)
783
+ ],
784
+ { cwd: stagingDir, timeoutMs: NPM_INSTALL_TIMEOUT_MS }
785
+ );
786
+ const entry = rootEntryPath(stagingDir, this.spec);
787
+ if (!fs3.existsSync(entry)) {
788
+ throw new Error(`staged closure is missing the root entry (${entry})`);
789
+ }
790
+ const stagedVersion = readPackageVersion(
791
+ path4.join(rootPackageDir(stagingDir, this.spec), "package.json")
792
+ );
793
+ if (stagedVersion !== target) {
794
+ throw new Error(
795
+ `staged closure version mismatch: expected ${target}, got ${stagedVersion ?? "unknown"}`
796
+ );
797
+ }
798
+ const dest = versionDir(rootDir, target);
799
+ if (fs3.existsSync(dest)) {
800
+ const aside = `${dest}.evicted-${this.now()}`;
801
+ await fs3.promises.rename(dest, aside);
802
+ await fs3.promises.rm(aside, { recursive: true, force: true }).catch(() => void 0);
803
+ }
804
+ await fs3.promises.rename(stagingDir, dest);
805
+ } catch (err) {
806
+ await fs3.promises.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
807
+ throw err;
808
+ }
809
+ }
810
+ // ── Rollback ──────────────────────────────────────────────────────────
811
+ async rollbackServerUpdate() {
812
+ if (this.inFlight !== "idle") {
813
+ return {
814
+ accepted: false,
815
+ targetVersion: null,
816
+ restarting: false,
817
+ message: `Refused: another operation is in flight (${this.inFlight}).`
818
+ };
819
+ }
820
+ const state = this.readState();
821
+ if (state.pendingBoot !== null) {
822
+ return {
823
+ accepted: false,
824
+ targetVersion: null,
825
+ restarting: false,
826
+ message: `Refused: version ${state.pendingBoot.version} is already staged and awaiting restart.`
827
+ };
828
+ }
829
+ if (state.currentVersion === null || state.previousVersion === null) {
830
+ return {
831
+ accepted: false,
832
+ targetVersion: state.previousVersion,
833
+ restarting: false,
834
+ message: "Refused: no previous version available to roll back to."
835
+ };
836
+ }
837
+ const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
838
+ const invalidReason = validateVersionDir(
839
+ this.rootDir(),
840
+ state.previousVersion,
841
+ nodeMajor,
842
+ this.spec
843
+ );
844
+ if (invalidReason !== null) {
845
+ return {
846
+ accepted: false,
847
+ targetVersion: state.previousVersion,
848
+ restarting: false,
849
+ message: `Refused: previous version ${state.previousVersion} is not loadable (${invalidReason}).`
850
+ };
851
+ }
852
+ writeServerRootState(this.rootDir(), {
853
+ ...state,
854
+ pendingBoot: {
855
+ version: state.previousVersion,
856
+ fromVersion: state.currentVersion,
857
+ requestedAtMs: this.now(),
858
+ bootAttempts: 0
859
+ }
860
+ });
861
+ this.logger.info("root package rollback requested \u2014 restarting", {
862
+ meta: { fromVersion: state.currentVersion, toVersion: state.previousVersion }
863
+ });
864
+ this.restartServerFn(
865
+ `${RESTART_REASON_PREFIX}: rollback to ${this.spec.packageName}@${state.previousVersion}`
866
+ );
867
+ return {
868
+ accepted: true,
869
+ targetVersion: state.previousVersion,
870
+ restarting: true,
871
+ message: `Rolling back to ${this.spec.packageName}@${state.previousVersion} \u2014 restarting.`
872
+ };
873
+ }
874
+ // ── Plain restart ─────────────────────────────────────────────────────
875
+ /**
876
+ * Plain process restart — no version change. Backs the `restartServer` cap
877
+ * method. Guards mirror apply/rollback: refuse while a stage is in flight
878
+ * (a concurrent npm install must not be interrupted) or while a version is
879
+ * already staged awaiting restart (a naive bounce would boot the OLD version
880
+ * and orphan the pending one — the operator should apply/rollback instead).
881
+ */
882
+ restartNode() {
883
+ const runningVersion = this.runningVersion();
884
+ if (this.inFlight !== "idle") {
885
+ return {
886
+ accepted: false,
887
+ targetVersion: runningVersion,
888
+ restarting: false,
889
+ message: `Refused: another operation is in flight (${this.inFlight}).`
890
+ };
891
+ }
892
+ const state = this.readState();
893
+ if (state.pendingBoot !== null) {
894
+ return {
895
+ accepted: false,
896
+ targetVersion: runningVersion,
897
+ restarting: false,
898
+ message: `Refused: version ${state.pendingBoot.version} is staged and awaiting restart \u2014 apply or roll it back instead of a plain restart.`
899
+ };
900
+ }
901
+ this.logger.info("plain node restart requested", { meta: { runningVersion } });
902
+ this.restartServerFn(`${RESTART_REASON_PREFIX}: manual restart`);
903
+ return {
904
+ accepted: true,
905
+ targetVersion: runningVersion,
906
+ restarting: true,
907
+ message: "Restarting the node \u2014 the supervisor relaunches it on the same version."
908
+ };
909
+ }
910
+ // ── Boot health confirmation ──────────────────────────────────────────
911
+ /**
912
+ * Called by the post-boot path once the node is READY (hub: post-boot
913
+ * pipeline; agent: first acked `$hub.registerNode`). When THIS process is
914
+ * the probation boot of a pending version, promote it: `currentVersion` ←
915
+ * pending, `previousVersion` ← the version it replaced (N-1 retained for
916
+ * rollback), clear `pendingBoot` + `rolledBack`, and prune every version
917
+ * dir beyond {current, previous}. No-op otherwise.
918
+ */
919
+ confirmBootHealthy() {
920
+ const state = readServerRootState(this.rootDir());
921
+ if (state === null || state.pendingBoot === null) return { promoted: null };
922
+ const activeVersion = this.env[this.envNames.activeVersion] ?? null;
923
+ if (activeVersion !== state.pendingBoot.version) {
924
+ return { promoted: null };
925
+ }
926
+ const promoted = {
927
+ ...state,
928
+ currentVersion: state.pendingBoot.version,
929
+ previousVersion: state.pendingBoot.fromVersion,
930
+ pendingBoot: null,
931
+ rolledBack: null
932
+ };
933
+ writeServerRootState(this.rootDir(), promoted);
934
+ this.pruneVersions(
935
+ [promoted.currentVersion, promoted.previousVersion].filter(
936
+ (v) => typeof v === "string"
937
+ )
938
+ );
939
+ this.logger.info("root package update confirmed healthy", {
940
+ meta: {
941
+ version: promoted.currentVersion,
942
+ previousVersion: promoted.previousVersion
943
+ }
944
+ });
945
+ return { promoted: promoted.currentVersion };
946
+ }
947
+ /**
948
+ * Remove version dirs under `versions/` not in `keep`.
949
+ *
950
+ * Transient dot-entries (`.staging-*` / `.evicted-*`) are NOT pruned unless
951
+ * older than {@link RootUpdateService.STALE_TRANSIENT_MS}: the API starts
952
+ * serving BEFORE the post-boot `confirmBootHealthy()` runs, so a concurrent
953
+ * `applyServerUpdate` can be mid-npm-install into a fresh `.staging-*` dir
954
+ * when this prune fires — deleting it would corrupt that install. Fresh
955
+ * transient dirs are owned by their in-flight operation (which cleans up on
956
+ * failure); the staleness sweep only collects dirs orphaned by a crash.
957
+ */
958
+ pruneVersions(keep) {
959
+ const vDir = versionsDir(this.rootDir());
960
+ let entries;
961
+ try {
962
+ entries = fs3.readdirSync(vDir);
963
+ } catch {
964
+ return;
965
+ }
966
+ for (const entry of entries) {
967
+ if (keep.includes(entry)) continue;
968
+ const full = path4.join(vDir, entry);
969
+ if (entry.startsWith(".")) {
970
+ try {
971
+ const ageMs = this.now() - fs3.statSync(full).mtimeMs;
972
+ if (ageMs < _RootUpdateService.STALE_TRANSIENT_MS) continue;
973
+ } catch {
974
+ continue;
975
+ }
976
+ }
977
+ try {
978
+ fs3.rmSync(full, { recursive: true, force: true });
979
+ this.logger.debug("pruned server-root version dir", { meta: { entry } });
980
+ } catch (err) {
981
+ this.logger.warn("failed to prune server-root version dir", {
982
+ meta: { entry, error: err instanceof Error ? err.message : String(err) }
983
+ });
984
+ }
985
+ }
986
+ }
987
+ /** Transient (.staging- / .evicted-) dirs younger than this are never pruned. */
988
+ static STALE_TRANSIENT_MS = 24 * 60 * 60 * 1e3;
989
+ };
990
+ }
991
+ });
992
+
993
+ export {
994
+ __require,
995
+ __toESM,
996
+ require_dist
997
+ };
998
+ //# sourceMappingURL=chunk-L34OOQI3.mjs.map