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