@camstack/server 1.2.124 → 1.2.126
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/agent/agent-update-service.js +93 -2
- package/dist/agent/main.js +3 -1
- package/dist/boot/post-boot.service.js +1 -0
- package/dist/core/addon/addon-package.service.js +17 -1
- package/dist/core/server-update/server-update.service.js +5 -1
- package/dist/core/update-availability-emitter.js +77 -18
- package/package.json +7 -7
|
@@ -60,11 +60,15 @@ exports.agentRuntimeManifestEntry = agentRuntimeManifestEntry;
|
|
|
60
60
|
*
|
|
61
61
|
* Spec: docs/superpowers/specs/2026-07-18-single-framework-copy-collapse-design.md
|
|
62
62
|
*/
|
|
63
|
+
const node_crypto_1 = require("node:crypto");
|
|
63
64
|
const fs = __importStar(require("node:fs"));
|
|
64
65
|
const path = __importStar(require("node:path"));
|
|
65
66
|
const index_js_1 = require("../server-root/index.js");
|
|
66
67
|
const system_exec_npm_js_1 = require("../core/server-update/system-exec-npm.js");
|
|
67
68
|
const system_ensure_prebuilds_js_1 = require("../core/server-update/system-ensure-prebuilds.js");
|
|
69
|
+
const system_1 = require("@camstack/system");
|
|
70
|
+
const types_1 = require("@camstack/types");
|
|
71
|
+
const update_availability_emitter_js_1 = require("../core/update-availability-emitter.js");
|
|
68
72
|
/**
|
|
69
73
|
* The synthetic addonId the agent's OWN runtime registers infra providers
|
|
70
74
|
* under (no addon owns them — the bootstrap does). Appears in the agent's
|
|
@@ -143,7 +147,13 @@ function scheduleAgentRestart(logger, requestedBy, dataDir) {
|
|
|
143
147
|
grace.unref();
|
|
144
148
|
}
|
|
145
149
|
class AgentUpdateService extends index_js_1.RootUpdateService {
|
|
150
|
+
updateAvailability;
|
|
151
|
+
eventBus;
|
|
152
|
+
nodeId;
|
|
153
|
+
agentDataDir;
|
|
146
154
|
constructor(options) {
|
|
155
|
+
const restartAgent = options.restartAgent ??
|
|
156
|
+
((requestedBy) => scheduleAgentRestart(options.logger, requestedBy, options.dataDir));
|
|
147
157
|
super({
|
|
148
158
|
spec: index_js_1.HUB_ROOT_SPEC,
|
|
149
159
|
// The agent's on-disk root IS `@camstack/server` (booted via
|
|
@@ -155,8 +165,10 @@ class AgentUpdateService extends index_js_1.RootUpdateService {
|
|
|
155
165
|
seedDir: 'CAMSTACK_SEED_SERVER_DIR',
|
|
156
166
|
},
|
|
157
167
|
logger: options.logger,
|
|
158
|
-
restartServer:
|
|
159
|
-
(
|
|
168
|
+
restartServer: (requestedBy) => {
|
|
169
|
+
writeAgentRestartMarker(options.dataDir, requestedBy);
|
|
170
|
+
restartAgent(requestedBy);
|
|
171
|
+
},
|
|
160
172
|
dataDir: options.dataDir,
|
|
161
173
|
runningPackageJsonPath: options.runningPackageJsonPath ?? resolveAgentPackageJsonPath(__dirname),
|
|
162
174
|
workspaceProbeDir: __dirname,
|
|
@@ -175,9 +187,88 @@ class AgentUpdateService extends index_js_1.RootUpdateService {
|
|
|
175
187
|
env: options.env,
|
|
176
188
|
now: options.now,
|
|
177
189
|
});
|
|
190
|
+
this.nodeId = options.nodeId ?? process.env['CAMSTACK_NODE_ID'] ?? 'agent';
|
|
191
|
+
this.agentDataDir = options.dataDir;
|
|
192
|
+
this.eventBus = options.eventBus ?? null;
|
|
193
|
+
this.updateAvailability =
|
|
194
|
+
options.eventBus !== undefined
|
|
195
|
+
? new update_availability_emitter_js_1.UpdateAvailabilityEmitter(options.eventBus, {
|
|
196
|
+
type: 'core',
|
|
197
|
+
id: 'agent-update-service',
|
|
198
|
+
})
|
|
199
|
+
: null;
|
|
200
|
+
}
|
|
201
|
+
bindAvailability(eventBus, nodeId) {
|
|
202
|
+
this.eventBus = eventBus;
|
|
203
|
+
this.nodeId = nodeId;
|
|
204
|
+
this.updateAvailability = new update_availability_emitter_js_1.UpdateAvailabilityEmitter(eventBus, {
|
|
205
|
+
type: 'core',
|
|
206
|
+
id: 'agent-update-service',
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
async checkServerUpdate() {
|
|
210
|
+
const result = await super.checkServerUpdate();
|
|
211
|
+
if (result.error !== null)
|
|
212
|
+
return result;
|
|
213
|
+
this.updateAvailability?.publishSnapshot('server', result.updateAvailable && result.runningVersion !== null && result.latestVersion !== null
|
|
214
|
+
? [
|
|
215
|
+
{
|
|
216
|
+
target: 'server',
|
|
217
|
+
packageName: result.packageName,
|
|
218
|
+
currentVersion: result.runningVersion,
|
|
219
|
+
latestVersion: result.latestVersion,
|
|
220
|
+
nodeId: this.nodeId,
|
|
221
|
+
},
|
|
222
|
+
]
|
|
223
|
+
: [], this.nodeId);
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
/** Consume `.restart-pending` and emit `system.restart-completed` if present. */
|
|
227
|
+
emitRestartCompletedIfPending() {
|
|
228
|
+
const bus = this.eventBus;
|
|
229
|
+
if (bus === null)
|
|
230
|
+
return;
|
|
231
|
+
const marker = (0, system_1.readPendingRestart)(this.agentDataDir);
|
|
232
|
+
if (marker === null)
|
|
233
|
+
return;
|
|
234
|
+
const payload = {
|
|
235
|
+
kind: marker.kind,
|
|
236
|
+
requestedAt: marker.requestedAt,
|
|
237
|
+
nodeId: this.nodeId,
|
|
238
|
+
...(marker.packageName !== undefined ? { packageName: marker.packageName } : {}),
|
|
239
|
+
...(marker.fromVersion !== undefined ? { fromVersion: marker.fromVersion } : {}),
|
|
240
|
+
...(marker.toVersion !== undefined ? { toVersion: marker.toVersion } : {}),
|
|
241
|
+
...(marker.requestedBy !== undefined ? { requestedBy: marker.requestedBy } : {}),
|
|
242
|
+
};
|
|
243
|
+
bus.emit({
|
|
244
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
245
|
+
timestamp: new Date(),
|
|
246
|
+
source: { type: 'core', id: 'agent-update-service' },
|
|
247
|
+
category: types_1.EventCategory.SystemRestartCompleted,
|
|
248
|
+
data: payload,
|
|
249
|
+
});
|
|
178
250
|
}
|
|
179
251
|
}
|
|
180
252
|
exports.AgentUpdateService = AgentUpdateService;
|
|
253
|
+
function writeAgentRestartMarker(dataDir, requestedBy) {
|
|
254
|
+
const toVersion = /@([^@\s]+)$/.exec(requestedBy)?.[1];
|
|
255
|
+
try {
|
|
256
|
+
(0, system_1.writePendingRestart)(dataDir, {
|
|
257
|
+
kind: requestedBy.startsWith('server-update:') ? 'framework-update' : 'manual',
|
|
258
|
+
requestedAt: Date.now(),
|
|
259
|
+
requestedBy,
|
|
260
|
+
...(requestedBy.startsWith('server-update:')
|
|
261
|
+
? {
|
|
262
|
+
packageName: '@camstack/server',
|
|
263
|
+
...(toVersion !== undefined ? { toVersion } : {}),
|
|
264
|
+
}
|
|
265
|
+
: {}),
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// Marker write must never block the restart.
|
|
270
|
+
}
|
|
271
|
+
}
|
|
181
272
|
/**
|
|
182
273
|
* The manifest entry for the agent runtime's own providers. Appended to
|
|
183
274
|
* `buildAgentOwnManifest`'s output so `server-management` reaches the hub's
|
package/dist/agent/main.js
CHANGED
|
@@ -378,6 +378,7 @@ async function startAgent(configPath) {
|
|
|
378
378
|
agentBootConfirmed = true;
|
|
379
379
|
try {
|
|
380
380
|
agentUpdateService.confirmBootHealthy();
|
|
381
|
+
agentUpdateService.emitRestartCompletedIfPending();
|
|
381
382
|
}
|
|
382
383
|
catch (err) {
|
|
383
384
|
consoleLogger.warn(`agent root boot confirmation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -655,9 +656,10 @@ async function startAgent(configPath) {
|
|
|
655
656
|
// local events propagate to every UDS child. Inert when agentUdsRegistry
|
|
656
657
|
// was not started (no UDS children). Wired after broker.start() so
|
|
657
658
|
// getBrokerEventBus returns the fully operational shared bus.
|
|
659
|
+
const agentBrokerEventBus = (0, system_1.getBrokerEventBus)(broker);
|
|
660
|
+
agentUpdateService.bindAvailability(agentBrokerEventBus, broker.nodeID);
|
|
658
661
|
let udsEventBridgeDispose = null;
|
|
659
662
|
if (agentUdsRegistry !== undefined) {
|
|
660
|
-
const agentBrokerEventBus = (0, system_1.getBrokerEventBus)(broker);
|
|
661
663
|
udsEventBridgeDispose = (0, system_1.createUdsEventBridge)({
|
|
662
664
|
registry: agentUdsRegistry,
|
|
663
665
|
parentBus: agentBrokerEventBus,
|
|
@@ -75,6 +75,7 @@ class PostBootService {
|
|
|
75
75
|
const payload = {
|
|
76
76
|
kind: marker.kind,
|
|
77
77
|
requestedAt: marker.requestedAt,
|
|
78
|
+
nodeId: process.env['CAMSTACK_NODE_ID'] ?? 'hub',
|
|
78
79
|
...(marker.packageName !== undefined ? { packageName: marker.packageName } : {}),
|
|
79
80
|
...(marker.fromVersion !== undefined ? { fromVersion: marker.fromVersion } : {}),
|
|
80
81
|
...(marker.toVersion !== undefined ? { toVersion: marker.toVersion } : {}),
|
|
@@ -1103,10 +1103,17 @@ class AddonPackageService {
|
|
|
1103
1103
|
*/
|
|
1104
1104
|
restartServer(requestedBy) {
|
|
1105
1105
|
this.logger.info('Server restart requested -- initiating graceful shutdown');
|
|
1106
|
+
const toVersion = toVersionFromRestartReason(requestedBy);
|
|
1106
1107
|
const payload = {
|
|
1107
|
-
kind: 'manual',
|
|
1108
|
+
kind: isServerUpdateRequest(requestedBy) ? 'framework-update' : 'manual',
|
|
1108
1109
|
requestedAt: Date.now(),
|
|
1109
1110
|
...(requestedBy !== undefined ? { requestedBy } : {}),
|
|
1111
|
+
...(isServerUpdateRequest(requestedBy)
|
|
1112
|
+
? {
|
|
1113
|
+
packageName: '@camstack/server',
|
|
1114
|
+
...(toVersion !== undefined ? { toVersion } : {}),
|
|
1115
|
+
}
|
|
1116
|
+
: {}),
|
|
1110
1117
|
};
|
|
1111
1118
|
try {
|
|
1112
1119
|
(0, system_1.writePendingRestart)(this.resolveDataDir(), payload);
|
|
@@ -1710,6 +1717,15 @@ class AddonPackageService {
|
|
|
1710
1717
|
}
|
|
1711
1718
|
}
|
|
1712
1719
|
exports.AddonPackageService = AddonPackageService;
|
|
1720
|
+
function isServerUpdateRequest(requestedBy) {
|
|
1721
|
+
return requestedBy !== undefined && requestedBy.startsWith('server-update:');
|
|
1722
|
+
}
|
|
1723
|
+
function toVersionFromRestartReason(requestedBy) {
|
|
1724
|
+
if (requestedBy === undefined)
|
|
1725
|
+
return undefined;
|
|
1726
|
+
const match = /@([^@\s]+)$/.exec(requestedBy);
|
|
1727
|
+
return match?.[1];
|
|
1728
|
+
}
|
|
1713
1729
|
// ---------------------------------------------------------------------------
|
|
1714
1730
|
// Tarball extraction helper (exported for regression tests)
|
|
1715
1731
|
// ---------------------------------------------------------------------------
|
|
@@ -58,6 +58,7 @@ const system_ensure_prebuilds_js_1 = require("./system-ensure-prebuilds.js");
|
|
|
58
58
|
const update_availability_emitter_js_1 = require("../update-availability-emitter.js");
|
|
59
59
|
class ServerUpdateService extends index_js_1.RootUpdateService {
|
|
60
60
|
updateAvailability;
|
|
61
|
+
nodeId;
|
|
61
62
|
constructor(options) {
|
|
62
63
|
const dataDir = path.resolve(options.dataDir ??
|
|
63
64
|
options.env?.['CAMSTACK_DATA'] ??
|
|
@@ -99,11 +100,13 @@ class ServerUpdateService extends index_js_1.RootUpdateService {
|
|
|
99
100
|
id: 'server-update-service',
|
|
100
101
|
})
|
|
101
102
|
: null;
|
|
103
|
+
this.nodeId = options.nodeId ?? 'hub';
|
|
102
104
|
}
|
|
103
105
|
async checkServerUpdate() {
|
|
104
106
|
const result = await super.checkServerUpdate();
|
|
105
107
|
if (result.error !== null)
|
|
106
108
|
return result;
|
|
109
|
+
const nodeId = this.nodeId;
|
|
107
110
|
this.updateAvailability?.publishSnapshot('server', result.updateAvailable && result.runningVersion !== null && result.latestVersion !== null
|
|
108
111
|
? [
|
|
109
112
|
{
|
|
@@ -111,9 +114,10 @@ class ServerUpdateService extends index_js_1.RootUpdateService {
|
|
|
111
114
|
packageName: result.packageName,
|
|
112
115
|
currentVersion: result.runningVersion,
|
|
113
116
|
latestVersion: result.latestVersion,
|
|
117
|
+
...(nodeId !== undefined ? { nodeId } : {}),
|
|
114
118
|
},
|
|
115
119
|
]
|
|
116
|
-
: []);
|
|
120
|
+
: [], nodeId);
|
|
117
121
|
return result;
|
|
118
122
|
}
|
|
119
123
|
}
|
|
@@ -3,14 +3,23 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.UpdateAvailabilityEmitter = void 0;
|
|
4
4
|
const types_1 = require("@camstack/types");
|
|
5
5
|
/**
|
|
6
|
-
* Emits update availability only when a successful check changes
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
6
|
+
* Emits update availability only when a successful check changes an observed
|
|
7
|
+
* `latestVersion` (or first-sees a package/node that has one). Repeated polls
|
|
8
|
+
* of the same latests are silent.
|
|
9
|
+
*
|
|
10
|
+
* Addon snapshots emit ONE event carrying the **entire** currently-available
|
|
11
|
+
* list. Server snapshots do the same across nodes, so the Notification Center
|
|
12
|
+
* can say "1.0.3 is available on hub and little-unraid" instead of one push
|
|
13
|
+
* per package or per node.
|
|
14
|
+
*
|
|
15
|
+
* A successful empty snapshot clears prior state for that scope; failed checks
|
|
16
|
+
* should not call this method.
|
|
9
17
|
*/
|
|
10
18
|
class UpdateAvailabilityEmitter {
|
|
11
19
|
eventBus;
|
|
12
20
|
source;
|
|
13
|
-
|
|
21
|
+
latestByKey = new Map();
|
|
22
|
+
held = new Map();
|
|
14
23
|
constructor(eventBus, source) {
|
|
15
24
|
this.eventBus = eventBus;
|
|
16
25
|
this.source = source;
|
|
@@ -18,36 +27,78 @@ class UpdateAvailabilityEmitter {
|
|
|
18
27
|
publishSnapshot(target, candidates, nodeId) {
|
|
19
28
|
const scope = nodeId ?? 'hub';
|
|
20
29
|
const present = new Set();
|
|
30
|
+
let latestChanged = false;
|
|
21
31
|
for (const candidate of candidates) {
|
|
22
32
|
if (candidate.target !== target)
|
|
23
33
|
continue;
|
|
24
|
-
const
|
|
34
|
+
const stamped = stampNode(candidate, nodeId);
|
|
35
|
+
const key = this.key(stamped);
|
|
25
36
|
present.add(key);
|
|
26
|
-
this.
|
|
37
|
+
if (this.note(stamped))
|
|
38
|
+
latestChanged = true;
|
|
27
39
|
}
|
|
28
|
-
for (const key of this.
|
|
29
|
-
if (key.startsWith(`${target}:${scope}:`)
|
|
30
|
-
|
|
31
|
-
|
|
40
|
+
for (const key of [...this.held.keys()]) {
|
|
41
|
+
if (!key.startsWith(`${target}:${scope}:`))
|
|
42
|
+
continue;
|
|
43
|
+
if (present.has(key))
|
|
44
|
+
continue;
|
|
45
|
+
this.held.delete(key);
|
|
46
|
+
this.latestByKey.delete(key);
|
|
32
47
|
}
|
|
48
|
+
if (!latestChanged)
|
|
49
|
+
return;
|
|
50
|
+
this.emitList(target);
|
|
33
51
|
}
|
|
34
52
|
/** Publish a partial candidate set without treating omissions as up-to-date. */
|
|
35
53
|
publishCandidates(candidates) {
|
|
36
|
-
|
|
37
|
-
|
|
54
|
+
let latestChanged = false;
|
|
55
|
+
const targets = new Set();
|
|
56
|
+
for (const candidate of candidates) {
|
|
57
|
+
targets.add(candidate.target);
|
|
58
|
+
if (this.note(candidate))
|
|
59
|
+
latestChanged = true;
|
|
60
|
+
}
|
|
61
|
+
if (!latestChanged)
|
|
62
|
+
return;
|
|
63
|
+
for (const target of targets)
|
|
64
|
+
this.emitList(target);
|
|
38
65
|
}
|
|
39
|
-
|
|
66
|
+
note(candidate) {
|
|
40
67
|
const key = this.key(candidate);
|
|
41
|
-
const
|
|
42
|
-
|
|
68
|
+
const previous = this.latestByKey.get(key);
|
|
69
|
+
this.held.set(key, candidate);
|
|
70
|
+
this.latestByKey.set(key, candidate.latestVersion);
|
|
71
|
+
return previous !== candidate.latestVersion;
|
|
72
|
+
}
|
|
73
|
+
emitList(target) {
|
|
74
|
+
const list = [...this.held.values()].filter((candidate) => candidate.target === target);
|
|
75
|
+
if (list.length === 0)
|
|
43
76
|
return;
|
|
44
|
-
|
|
77
|
+
const packages = list.map((candidate) => ({
|
|
78
|
+
packageName: candidate.packageName,
|
|
79
|
+
currentVersion: candidate.currentVersion,
|
|
80
|
+
latestVersion: candidate.latestVersion,
|
|
81
|
+
...(candidate.nodeId !== undefined ? { nodeId: candidate.nodeId } : {}),
|
|
82
|
+
}));
|
|
83
|
+
const nodeIds = unique(packages.map((pkg) => pkg.nodeId).filter((id) => id !== undefined));
|
|
84
|
+
const head = list[0];
|
|
45
85
|
this.eventBus.emit({
|
|
46
|
-
id: `update.available:${
|
|
86
|
+
id: `update.available:${target}:${packages
|
|
87
|
+
.map((pkg) => `${pkg.nodeId ?? 'hub'}:${pkg.packageName}@${pkg.latestVersion}`)
|
|
88
|
+
.toSorted()
|
|
89
|
+
.join(',')}`,
|
|
47
90
|
timestamp: new Date(),
|
|
48
91
|
source: this.source,
|
|
49
92
|
category: types_1.EventCategory.UpdateAvailable,
|
|
50
|
-
data:
|
|
93
|
+
data: {
|
|
94
|
+
target,
|
|
95
|
+
packageName: head.packageName,
|
|
96
|
+
currentVersion: head.currentVersion,
|
|
97
|
+
latestVersion: head.latestVersion,
|
|
98
|
+
packages,
|
|
99
|
+
...(head.nodeId !== undefined ? { nodeId: head.nodeId } : {}),
|
|
100
|
+
...(nodeIds.length > 0 ? { nodeIds } : {}),
|
|
101
|
+
},
|
|
51
102
|
});
|
|
52
103
|
}
|
|
53
104
|
key(candidate) {
|
|
@@ -55,3 +106,11 @@ class UpdateAvailabilityEmitter {
|
|
|
55
106
|
}
|
|
56
107
|
}
|
|
57
108
|
exports.UpdateAvailabilityEmitter = UpdateAvailabilityEmitter;
|
|
109
|
+
function stampNode(candidate, nodeId) {
|
|
110
|
+
if (nodeId === undefined || candidate.nodeId !== undefined)
|
|
111
|
+
return candidate;
|
|
112
|
+
return { ...candidate, nodeId };
|
|
113
|
+
}
|
|
114
|
+
function unique(values) {
|
|
115
|
+
return [...new Set(values)];
|
|
116
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.126",
|
|
4
4
|
"private": false,
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -33,18 +33,18 @@
|
|
|
33
33
|
]
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@camstack/addon-admin-ui": "1.2.
|
|
36
|
+
"@camstack/addon-admin-ui": "1.2.68",
|
|
37
37
|
"@camstack/addon-agent-ui": "1.2.20",
|
|
38
38
|
"@camstack/addon-auth": "1.2.22",
|
|
39
39
|
"@camstack/addon-decoder-nodeav": "1.2.19",
|
|
40
40
|
"@camstack/addon-notifiers": "1.2.24",
|
|
41
|
-
"@camstack/addon-pipeline": "1.2.
|
|
42
|
-
"@camstack/addon-pipeline-orchestrator": "1.2.
|
|
43
|
-
"@camstack/addon-post-analysis": "1.2.
|
|
41
|
+
"@camstack/addon-pipeline": "1.2.89",
|
|
42
|
+
"@camstack/addon-pipeline-orchestrator": "1.2.69",
|
|
43
|
+
"@camstack/addon-post-analysis": "1.2.85",
|
|
44
44
|
"@camstack/sdk": "1.2.22",
|
|
45
45
|
"@camstack/shm-ring": "1.1.19",
|
|
46
|
-
"@camstack/system": "1.2.
|
|
47
|
-
"@camstack/types": "1.2.
|
|
46
|
+
"@camstack/system": "1.2.102",
|
|
47
|
+
"@camstack/types": "1.2.83",
|
|
48
48
|
"@camstack/ui-library": "1.2.57",
|
|
49
49
|
"@fastify/compress": "^9.0.0",
|
|
50
50
|
"@fastify/cookie": "^11.0.2",
|