@camstack/server 0.1.8 → 0.2.1
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/package.json +9 -7
- package/src/__tests__/addon-install-e2e.test.ts +0 -1
- package/src/__tests__/addon-pages-e2e.test.ts +40 -18
- package/src/__tests__/addon-settings-router.spec.ts +6 -1
- package/src/__tests__/addon-upload.spec.ts +91 -29
- package/src/__tests__/agent-registry.spec.ts +26 -9
- package/src/__tests__/agent-status-page.spec.ts +1 -3
- package/src/__tests__/auth-session-cookie.test.ts +28 -1
- package/src/__tests__/bulk-update-coordinator.spec.ts +48 -31
- package/src/__tests__/cap-ownership-authority.spec.ts +39 -8
- package/src/__tests__/cap-providers/cap-providers-location-import.spec.ts +24 -4
- package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +17 -3
- package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +57 -11
- package/src/__tests__/cap-providers/integrations-delete-cascade.spec.ts +64 -15
- package/src/__tests__/cap-providers-bulk-update.spec.ts +27 -7
- package/src/__tests__/cap-route-adapter.spec.ts +28 -15
- package/src/__tests__/cap-routers/_meta.spec.ts +6 -7
- package/src/__tests__/cap-routers/addon-settings.router.spec.ts +19 -10
- package/src/__tests__/cap-routers/broker-routing.router.spec.ts +14 -6
- package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +3 -1
- package/src/__tests__/cap-routers/capabilities-node.spec.ts +18 -5
- package/src/__tests__/cap-routers/device-link-overlay.spec.ts +11 -6
- package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +72 -20
- package/src/__tests__/cap-routers/harness.ts +11 -7
- package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +17 -3
- package/src/__tests__/cap-routers/null-provider-guard.spec.ts +5 -7
- package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +35 -11
- package/src/__tests__/cap-routers/settings-store.router.spec.ts +59 -15
- package/src/__tests__/capability-e2e.test.ts +9 -11
- package/src/__tests__/cli-e2e.test.ts +80 -59
- package/src/__tests__/core-cap-bridge.spec.ts +3 -1
- package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +12 -2
- package/src/__tests__/device-settings-contribution-dispatch.spec.ts +61 -30
- package/src/__tests__/embedded-deps-e2e.test.ts +35 -19
- package/src/__tests__/event-bus-proxy-router.spec.ts +3 -0
- package/src/__tests__/framework-allowlist.spec.ts +5 -4
- package/src/__tests__/https-e2e.test.ts +12 -6
- package/src/__tests__/lifecycle-e2e.test.ts +60 -11
- package/src/__tests__/live-events-subscription.spec.ts +17 -18
- package/src/__tests__/moleculer/uds-readiness.spec.ts +11 -4
- package/src/__tests__/moleculer/uds-topology.spec.ts +39 -11
- package/src/__tests__/moleculer/uds-unowned-call.spec.ts +71 -17
- package/src/__tests__/moleculer-register-node-idempotency.spec.ts +16 -7
- package/src/__tests__/native-cap-route.spec.ts +42 -19
- package/src/__tests__/oauth2-account-linking.spec.ts +63 -17
- package/src/__tests__/singleton-contention.test.ts +23 -11
- package/src/__tests__/streaming-diagnostic.test.ts +156 -53
- package/src/__tests__/streaming-scale.test.ts +69 -35
- package/src/__tests__/uds-addon-call-wiring.spec.ts +6 -1
- package/src/agent-status-page.ts +4 -3
- package/src/api/__tests__/addons-custom.spec.ts +22 -8
- package/src/api/__tests__/capabilities.router.test.ts +18 -9
- package/src/api/addon-upload.ts +46 -15
- package/src/api/addons-custom.router.ts +7 -6
- package/src/api/auth-whoami.ts +3 -1
- package/src/api/bridge-addons.router.ts +3 -1
- package/src/api/capabilities.router.ts +117 -78
- package/src/api/core/__tests__/auth-router-totp.spec.ts +57 -16
- package/src/api/core/addon-settings.router.ts +4 -1
- package/src/api/core/agents.router.ts +52 -53
- package/src/api/core/auth.router.ts +55 -36
- package/src/api/core/bulk-update-coordinator.ts +25 -22
- package/src/api/core/cap-providers.ts +346 -202
- package/src/api/core/capabilities.router.ts +30 -23
- package/src/api/core/hwaccel.router.ts +37 -10
- package/src/api/core/live-events.router.ts +16 -9
- package/src/api/core/logs.router.ts +54 -25
- package/src/api/core/notifications.router.ts +2 -1
- package/src/api/core/repl.router.ts +1 -3
- package/src/api/core/settings-backend.router.ts +68 -70
- package/src/api/core/system-events.router.ts +41 -32
- package/src/api/health/health.routes.ts +7 -13
- package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +12 -2
- package/src/api/oauth2/consent-page.ts +4 -3
- package/src/api/oauth2/oauth2-routes.ts +41 -12
- package/src/api/trpc/__tests__/scope-access-device.spec.ts +68 -23
- package/src/api/trpc/__tests__/scope-access.spec.ts +8 -13
- package/src/api/trpc/__tests__/webrtc-session-ua-enrich.spec.ts +10 -2
- package/src/api/trpc/cap-mount-helpers.ts +64 -55
- package/src/api/trpc/cap-route-error-formatter.ts +17 -9
- package/src/api/trpc/core-cap-bridge.ts +3 -1
- package/src/api/trpc/generated-cap-mounts.ts +593 -351
- package/src/api/trpc/generated-cap-routers.ts +3680 -579
- package/src/api/trpc/scope-access.ts +7 -7
- package/src/api/trpc/trpc.context.ts +7 -4
- package/src/api/trpc/trpc.middleware.ts +4 -2
- package/src/api/trpc/trpc.router.ts +79 -46
- package/src/auth/session-cookie.ts +10 -0
- package/src/boot/__tests__/integration-id-backfill.spec.ts +21 -6
- package/src/boot/boot-config.ts +103 -122
- package/src/boot/post-boot.service.ts +5 -3
- package/src/core/addon/__tests__/addon-registry-capability.test.ts +12 -3
- package/src/core/addon/addon-call-gateway.ts +20 -6
- package/src/core/addon/addon-package.service.ts +183 -89
- package/src/core/addon/addon-registry.service.ts +1163 -1305
- package/src/core/addon/addon-search.service.ts +2 -1
- package/src/core/addon/addon-settings-provider.ts +27 -7
- package/src/core/addon-bridge/addon-bridge.service.ts +11 -6
- package/src/core/addon-pages/addon-pages.service.ts +3 -1
- package/src/core/addon-widgets/addon-widgets.service.ts +5 -2
- package/src/core/agent/agent-registry.service.ts +60 -38
- package/src/core/auth/auth.service.spec.ts +6 -8
- package/src/core/config/config.service.spec.ts +1 -1
- package/src/core/events/event-bus.service.spec.ts +44 -21
- package/src/core/events/event-bus.service.ts +5 -1
- package/src/core/feature/feature.service.spec.ts +4 -1
- package/src/core/lifecycle/lifecycle-state-machine.spec.ts +8 -10
- package/src/core/logging/logging.service.spec.ts +61 -21
- package/src/core/logging/logging.service.ts +12 -3
- package/src/core/moleculer/cap-call-fn.spec.ts +17 -10
- package/src/core/moleculer/cap-call-fn.ts +5 -1
- package/src/core/moleculer/cap-route-authority.ts +18 -6
- package/src/core/moleculer/moleculer.service.ts +120 -32
- package/src/core/network/network-quality.service.spec.ts +6 -1
- package/src/core/notification/notification-wrapper.service.ts +1 -3
- package/src/core/notification/toast-wrapper.service.ts +1 -5
- package/src/core/repl/repl-engine.service.spec.ts +66 -39
- package/src/core/repl/repl-engine.service.ts +11 -12
- package/src/core/storage/storage-location-manager.spec.ts +12 -3
- package/src/core/streaming/stream-probe.service.ts +22 -13
- package/src/core/topology/topology-emitter.service.ts +5 -1
- package/src/launcher.ts +14 -9
- package/src/main.ts +602 -531
- package/src/manual-boot.ts +133 -154
- package/tsconfig.json +20 -8
|
@@ -44,7 +44,7 @@ import type {
|
|
|
44
44
|
IBrokerProvider,
|
|
45
45
|
CapabilityMethodAuth,
|
|
46
46
|
} from '@camstack/types'
|
|
47
|
-
import { asJsonObject, asJsonArray, errMsg } from '@camstack/types'
|
|
47
|
+
import { asJsonObject, asJsonArray, errMsg, EventCategory } from '@camstack/types'
|
|
48
48
|
import type { CapabilityRegistry } from '@camstack/kernel'
|
|
49
49
|
import { getCapUsageRegistry } from '@camstack/kernel'
|
|
50
50
|
import type { ToastService, NotificationService } from '@camstack/core'
|
|
@@ -68,7 +68,9 @@ const execFileAsync = promisify(execFile)
|
|
|
68
68
|
// ── system ──────────────────────────────────────────────────────────
|
|
69
69
|
|
|
70
70
|
function getRetention(registry: CapabilityRegistry | null) {
|
|
71
|
-
return
|
|
71
|
+
return (
|
|
72
|
+
registry?.getSingleton<IAnalysisDataPersistence>('analysis-data-persistence')?.retention ?? null
|
|
73
|
+
)
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
export function buildSystemProvider(
|
|
@@ -107,9 +109,7 @@ export function buildSystemProvider(
|
|
|
107
109
|
|
|
108
110
|
// ── network-quality ─────────────────────────────────────────────────
|
|
109
111
|
|
|
110
|
-
export function buildNetworkQualityProvider(
|
|
111
|
-
nq: NetworkQualityService,
|
|
112
|
-
): INetworkQualityProvider {
|
|
112
|
+
export function buildNetworkQualityProvider(nq: NetworkQualityService): INetworkQualityProvider {
|
|
113
113
|
return {
|
|
114
114
|
getDeviceStats: async (input) => nq.getDeviceStats(input.deviceId),
|
|
115
115
|
getAllStats: async () => nq.getAllStats(),
|
|
@@ -141,7 +141,9 @@ export function buildToastProvider(
|
|
|
141
141
|
if (!toastService) return () => {}
|
|
142
142
|
const userId = ctx.user?.id ?? 'anonymous'
|
|
143
143
|
const connectionId = randomUUID()
|
|
144
|
-
const unsubscribe = toastService.subscribe(connectionId, userId, (toast: Toast) =>
|
|
144
|
+
const unsubscribe = toastService.subscribe(connectionId, userId, (toast: Toast) =>
|
|
145
|
+
push(toast),
|
|
146
|
+
)
|
|
145
147
|
return unsubscribe ?? (() => {})
|
|
146
148
|
},
|
|
147
149
|
}
|
|
@@ -184,14 +186,14 @@ export async function computeTopology(
|
|
|
184
186
|
): Promise<readonly TopologyNode[]> {
|
|
185
187
|
const nodes = await agentRegistry.listNodes()
|
|
186
188
|
const allAddons = addonRegistry?.listAddons() ?? []
|
|
187
|
-
const getInGroupAddonIds = (node: typeof nodes[number]): readonly string[] => {
|
|
189
|
+
const getInGroupAddonIds = (node: (typeof nodes)[number]): readonly string[] => {
|
|
188
190
|
const subs = (node.subProcesses ?? []) as readonly SubProcessLite[]
|
|
189
191
|
return subs.flatMap((p) => p.addonIds ?? [])
|
|
190
192
|
}
|
|
191
193
|
const addonCaps = new Map<string, readonly string[]>()
|
|
192
194
|
for (const a of allAddons) {
|
|
193
195
|
const id = a.manifest?.id ?? ''
|
|
194
|
-
const caps = a.declaration?.capabilities?.map(c => typeof c === 'string' ? c : c.name) ?? []
|
|
196
|
+
const caps = a.declaration?.capabilities?.map((c) => (typeof c === 'string' ? c : c.name)) ?? []
|
|
195
197
|
addonCaps.set(id, caps)
|
|
196
198
|
}
|
|
197
199
|
const addonCategory = new Map<string, string>()
|
|
@@ -202,7 +204,8 @@ export async function computeTopology(
|
|
|
202
204
|
}
|
|
203
205
|
return nodes.map((node) => {
|
|
204
206
|
const inGroupAddonIds = new Set(getInGroupAddonIds(node))
|
|
205
|
-
const agentAddonIds: readonly string[] =
|
|
207
|
+
const agentAddonIds: readonly string[] =
|
|
208
|
+
(node as { agentAddons?: readonly string[] }).agentAddons ?? []
|
|
206
209
|
type NodeAddonEntry = { id: string; capabilities: readonly string[]; status: 'running' }
|
|
207
210
|
const allNodeAddons: NodeAddonEntry[] = node.isHub
|
|
208
211
|
? allAddons.map((a) => {
|
|
@@ -215,31 +218,33 @@ export async function computeTopology(
|
|
|
215
218
|
status: 'running' as const,
|
|
216
219
|
}))
|
|
217
220
|
const inProcessAddons = allNodeAddons.filter((a) => !inGroupAddonIds.has(a.id))
|
|
218
|
-
const isolatedProcesses = (
|
|
219
|
-
const mainProcessServices = inProcessAddons.map(a => ({
|
|
221
|
+
const isolatedProcesses = (node.subProcesses ?? []) as readonly SubProcessLite[]
|
|
222
|
+
const mainProcessServices = inProcessAddons.map((a) => ({
|
|
220
223
|
addonId: a.id,
|
|
221
224
|
capabilities: a.capabilities,
|
|
222
225
|
status: a.status,
|
|
223
226
|
}))
|
|
224
|
-
const mainProcess = node.isHub
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
227
|
+
const mainProcess = node.isHub
|
|
228
|
+
? {
|
|
229
|
+
pid: process.pid,
|
|
230
|
+
name: 'hub (core)',
|
|
231
|
+
state: 'running' as const,
|
|
232
|
+
cpuPercent: node.status?.cpuPercent ?? 0,
|
|
233
|
+
memoryRss: process.memoryUsage().rss,
|
|
234
|
+
uptimeSeconds: Math.floor(process.uptime()),
|
|
235
|
+
services: mainProcessServices,
|
|
236
|
+
}
|
|
237
|
+
: {
|
|
238
|
+
pid: 0,
|
|
239
|
+
name: `${node.info.id} (core)`,
|
|
240
|
+
state: 'running' as const,
|
|
241
|
+
cpuPercent: node.status?.cpuPercent ?? 0,
|
|
242
|
+
memoryRss: 0,
|
|
243
|
+
uptimeSeconds: Math.floor((Date.now() - node.connectedSince) / 1000),
|
|
244
|
+
services: mainProcessServices,
|
|
245
|
+
}
|
|
241
246
|
const childProcesses = isolatedProcesses.map((p) => {
|
|
242
|
-
const memberIds =
|
|
247
|
+
const memberIds = p.addonIds && p.addonIds.length > 0 ? p.addonIds : [p.name]
|
|
243
248
|
return {
|
|
244
249
|
pid: p.pid,
|
|
245
250
|
name: p.name,
|
|
@@ -258,16 +263,23 @@ export async function computeTopology(
|
|
|
258
263
|
// Aggregate node-local addons by category. `allNodeAddons` already
|
|
259
264
|
// contains the per-node addon roster (hub uses every installed
|
|
260
265
|
// addon; agents use their assigned agentAddons subset).
|
|
261
|
-
const byCategory = new Map<
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
266
|
+
const byCategory = new Map<
|
|
267
|
+
string,
|
|
268
|
+
{
|
|
269
|
+
category: string
|
|
270
|
+
total: number
|
|
271
|
+
healthy: number
|
|
272
|
+
addons: { id: string; status: string; cpuPercent: number; memoryRss: number }[]
|
|
273
|
+
}
|
|
274
|
+
>()
|
|
267
275
|
const procByAddon = new Map<string, { cpuPercent: number; memoryRss: number; state: string }>()
|
|
268
276
|
for (const p of (node.subProcesses ?? []) as readonly SubProcessLite[]) {
|
|
269
|
-
for (const addonId of
|
|
270
|
-
procByAddon.set(addonId, {
|
|
277
|
+
for (const addonId of p.addonIds ?? []) {
|
|
278
|
+
procByAddon.set(addonId, {
|
|
279
|
+
cpuPercent: p.cpuPercent,
|
|
280
|
+
memoryRss: p.memoryRss,
|
|
281
|
+
state: p.state,
|
|
282
|
+
})
|
|
271
283
|
}
|
|
272
284
|
}
|
|
273
285
|
for (const a of allNodeAddons) {
|
|
@@ -304,10 +316,7 @@ export async function computeTopology(
|
|
|
304
316
|
lastSeen: new Date().toISOString(),
|
|
305
317
|
localIps: node.isHub ? getLocalIps() : (node.localIps ?? []),
|
|
306
318
|
addons: allNodeAddons,
|
|
307
|
-
processes: [
|
|
308
|
-
mainProcess,
|
|
309
|
-
...childProcesses,
|
|
310
|
-
],
|
|
319
|
+
processes: [mainProcess, ...childProcesses],
|
|
311
320
|
categories: categoriesProjection,
|
|
312
321
|
}
|
|
313
322
|
})
|
|
@@ -322,7 +331,11 @@ export async function computeTopology(
|
|
|
322
331
|
* `addon-registry.service.ts`; consider hoisting if a third call site appears.
|
|
323
332
|
*/
|
|
324
333
|
interface BrokerLike {
|
|
325
|
-
call<T = unknown>(
|
|
334
|
+
call<T = unknown>(
|
|
335
|
+
action: string,
|
|
336
|
+
params?: unknown,
|
|
337
|
+
opts?: { nodeID?: string; timeout?: number },
|
|
338
|
+
): Promise<T>
|
|
326
339
|
}
|
|
327
340
|
|
|
328
341
|
export function buildNodesProvider(
|
|
@@ -338,9 +351,13 @@ export function buildNodesProvider(
|
|
|
338
351
|
return { success: true }
|
|
339
352
|
},
|
|
340
353
|
undeployAddon: async (input) => {
|
|
341
|
-
await broker.call(
|
|
342
|
-
|
|
343
|
-
|
|
354
|
+
await broker.call(
|
|
355
|
+
'$agent.undeploy',
|
|
356
|
+
{
|
|
357
|
+
addonId: input.addonId,
|
|
358
|
+
},
|
|
359
|
+
{ nodeID: input.nodeId, timeout: 30_000 },
|
|
360
|
+
)
|
|
344
361
|
return { success: true }
|
|
345
362
|
},
|
|
346
363
|
restartAddon: async (input) => {
|
|
@@ -353,31 +370,47 @@ export function buildNodesProvider(
|
|
|
353
370
|
return { success: true }
|
|
354
371
|
}
|
|
355
372
|
const agentNodeId = input.nodeId.includes('/') ? input.nodeId.split('/')[0]! : input.nodeId
|
|
356
|
-
await broker.call(
|
|
357
|
-
|
|
358
|
-
|
|
373
|
+
await broker.call(
|
|
374
|
+
'$agent.restart',
|
|
375
|
+
{
|
|
376
|
+
addonId: input.addonId,
|
|
377
|
+
},
|
|
378
|
+
{ nodeID: agentNodeId, timeout: 30_000 },
|
|
379
|
+
)
|
|
359
380
|
return { success: true }
|
|
360
381
|
},
|
|
361
382
|
restartProcess: async (input) => {
|
|
362
|
-
return await broker.call(
|
|
363
|
-
|
|
364
|
-
|
|
383
|
+
return (await broker.call(
|
|
384
|
+
'$process.restart',
|
|
385
|
+
{
|
|
386
|
+
name: input.processName,
|
|
387
|
+
},
|
|
388
|
+
{ nodeID: input.nodeId, timeout: 30_000 },
|
|
389
|
+
)) as { success: boolean; reason?: string }
|
|
365
390
|
},
|
|
366
391
|
restartNode: async (input) => {
|
|
367
|
-
return await broker.call(
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
392
|
+
return (await broker.call(
|
|
393
|
+
'$process.restartAll',
|
|
394
|
+
{},
|
|
395
|
+
{
|
|
396
|
+
nodeID: input.nodeId,
|
|
397
|
+
timeout: 60_000,
|
|
398
|
+
},
|
|
399
|
+
)) as { restarted: readonly string[]; failed: readonly string[] }
|
|
371
400
|
},
|
|
372
401
|
shutdownNode: async (input) => {
|
|
373
402
|
if (input.nodeId === 'hub') {
|
|
374
403
|
setTimeout(() => process.exit(0), 500)
|
|
375
404
|
return { success: true }
|
|
376
405
|
}
|
|
377
|
-
await broker.call(
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
406
|
+
await broker.call(
|
|
407
|
+
'$agent.shutdown',
|
|
408
|
+
{},
|
|
409
|
+
{
|
|
410
|
+
nodeID: input.nodeId,
|
|
411
|
+
timeout: 10_000,
|
|
412
|
+
},
|
|
413
|
+
)
|
|
381
414
|
return { success: true }
|
|
382
415
|
},
|
|
383
416
|
renameNode: async (input) => {
|
|
@@ -390,9 +423,13 @@ export function buildNodesProvider(
|
|
|
390
423
|
value: trimmed,
|
|
391
424
|
})
|
|
392
425
|
} else {
|
|
393
|
-
await broker.call(
|
|
394
|
-
|
|
395
|
-
|
|
426
|
+
await broker.call(
|
|
427
|
+
'$agent.rename',
|
|
428
|
+
{
|
|
429
|
+
name: trimmed,
|
|
430
|
+
},
|
|
431
|
+
{ nodeID: input.nodeId, timeout: 10_000 },
|
|
432
|
+
)
|
|
396
433
|
agentRegistry.updateAgentName(input.nodeId, trimmed)
|
|
397
434
|
}
|
|
398
435
|
return { nodeId: input.nodeId, name: trimmed }
|
|
@@ -439,17 +476,50 @@ export function buildNodesProvider(
|
|
|
439
476
|
const remoteStatuses = await Promise.all(
|
|
440
477
|
remoteNodes.map(async (node) => {
|
|
441
478
|
try {
|
|
442
|
-
const status = await broker.call(
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
479
|
+
const status = (await broker.call(
|
|
480
|
+
'$agent.status',
|
|
481
|
+
{},
|
|
482
|
+
{
|
|
483
|
+
nodeID: node.info.id,
|
|
484
|
+
timeout: 5_000,
|
|
485
|
+
},
|
|
486
|
+
)) as {
|
|
487
|
+
addons?: readonly {
|
|
488
|
+
id: string
|
|
489
|
+
status: string
|
|
490
|
+
version?: string
|
|
491
|
+
packageName?: string
|
|
492
|
+
}[]
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
nodeId: node.info.id,
|
|
496
|
+
name: node.info.name,
|
|
497
|
+
online: true,
|
|
498
|
+
addons: status.addons ?? [],
|
|
499
|
+
}
|
|
446
500
|
} catch {
|
|
447
|
-
return {
|
|
501
|
+
return {
|
|
502
|
+
nodeId: node.info.id,
|
|
503
|
+
name: node.info.name,
|
|
504
|
+
online: false,
|
|
505
|
+
addons: [] as readonly {
|
|
506
|
+
id: string
|
|
507
|
+
status: string
|
|
508
|
+
version?: string
|
|
509
|
+
packageName?: string
|
|
510
|
+
}[],
|
|
511
|
+
}
|
|
448
512
|
}
|
|
449
513
|
}),
|
|
450
514
|
)
|
|
451
515
|
|
|
452
|
-
type NodeDeployment = {
|
|
516
|
+
type NodeDeployment = {
|
|
517
|
+
nodeId: string
|
|
518
|
+
name: string
|
|
519
|
+
version: string
|
|
520
|
+
status: string
|
|
521
|
+
synced: boolean
|
|
522
|
+
}
|
|
453
523
|
const result: Record<string, { hubVersion: string; nodes: NodeDeployment[] }> = {}
|
|
454
524
|
|
|
455
525
|
for (const [addonId, hubAddon] of hubMap) {
|
|
@@ -489,9 +559,13 @@ export function buildNodesProvider(
|
|
|
489
559
|
// also work — both are safe to run in parallel during Phase E.
|
|
490
560
|
const reachedViaUds = moleculer.setChildLogLevelByNodeId(input.nodeId, input.level)
|
|
491
561
|
if (!reachedViaUds) {
|
|
492
|
-
await broker.call(
|
|
493
|
-
|
|
494
|
-
|
|
562
|
+
await broker.call(
|
|
563
|
+
'$node-mgmt.setLogLevel',
|
|
564
|
+
{
|
|
565
|
+
level: input.level,
|
|
566
|
+
},
|
|
567
|
+
{ nodeID: input.nodeId, timeout: 5_000 },
|
|
568
|
+
)
|
|
495
569
|
}
|
|
496
570
|
return { success: true }
|
|
497
571
|
},
|
|
@@ -518,9 +592,11 @@ function requireIntegrationRegistry(ar: AddonRegistryService): IIntegrationRegis
|
|
|
518
592
|
}
|
|
519
593
|
|
|
520
594
|
function isDeviceProvider(value: unknown): value is IDeviceProvider {
|
|
521
|
-
return
|
|
522
|
-
|
|
523
|
-
|
|
595
|
+
return (
|
|
596
|
+
value !== null &&
|
|
597
|
+
typeof value === 'object' &&
|
|
598
|
+
typeof Reflect.get(value, 'discoverDevices') === 'function'
|
|
599
|
+
)
|
|
524
600
|
}
|
|
525
601
|
|
|
526
602
|
function getDeviceProvider(ar: AddonRegistryService, addonId: string): IDeviceProvider | null {
|
|
@@ -554,8 +630,7 @@ export function buildIntegrationsProvider(
|
|
|
554
630
|
const withProcessState = (i: Integration): IntegrationWithProcessState => ({
|
|
555
631
|
...i,
|
|
556
632
|
processState:
|
|
557
|
-
ar.listAddons().find(a => a.manifest.id === i.addonId)?.process?.state
|
|
558
|
-
?? 'unknown',
|
|
633
|
+
ar.listAddons().find((a) => a.manifest.id === i.addonId)?.process?.state ?? 'unknown',
|
|
559
634
|
})
|
|
560
635
|
|
|
561
636
|
return {
|
|
@@ -568,25 +643,27 @@ export function buildIntegrationsProvider(
|
|
|
568
643
|
if (!integration) throw new Error(`Integration "${input.id}" not found`)
|
|
569
644
|
return withProcessState(integration)
|
|
570
645
|
},
|
|
571
|
-
getByAddonId: async (input) =>
|
|
646
|
+
getByAddonId: async (input) =>
|
|
647
|
+
requireIntegrationRegistry(ar).getIntegrationByAddonId(input.addonId),
|
|
572
648
|
create: async (input) => {
|
|
573
649
|
const { skipRestart, ...payload } = input
|
|
574
650
|
const reg = requireIntegrationRegistry(ar)
|
|
575
651
|
|
|
576
|
-
logger.info('request', {
|
|
652
|
+
logger.info('request', {
|
|
653
|
+
tags: { addonId: input.addonId },
|
|
654
|
+
meta: { phase: 'create', name: input.name },
|
|
655
|
+
})
|
|
577
656
|
|
|
578
657
|
const addon = ar.listAddons().find((a) => a.manifest.id === input.addonId)
|
|
579
658
|
const instanceMode =
|
|
580
659
|
addon?.declaration?.instanceMode ?? addon?.manifest?.instanceMode ?? 'multiple'
|
|
581
660
|
if (instanceMode === 'unique') {
|
|
582
|
-
const existing = (await reg.listIntegrations()).filter(
|
|
583
|
-
(i) => i.addonId === input.addonId,
|
|
584
|
-
)
|
|
661
|
+
const existing = (await reg.listIntegrations()).filter((i) => i.addonId === input.addonId)
|
|
585
662
|
if (existing.length > 0) {
|
|
586
|
-
logger.warn(
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
)
|
|
663
|
+
logger.warn('rejected duplicate unique', {
|
|
664
|
+
tags: { addonId: input.addonId, integrationId: existing[0]!.id },
|
|
665
|
+
meta: { phase: 'create' },
|
|
666
|
+
})
|
|
590
667
|
throw new Error(
|
|
591
668
|
`Addon "${input.addonId}" is unique-instance and already has an integration (${existing[0]!.id})`,
|
|
592
669
|
)
|
|
@@ -594,15 +671,26 @@ export function buildIntegrationsProvider(
|
|
|
594
671
|
}
|
|
595
672
|
|
|
596
673
|
const integration = await reg.createIntegration(payload)
|
|
597
|
-
logger.info('persisted', {
|
|
674
|
+
logger.info('persisted', {
|
|
675
|
+
tags: { integrationId: integration.id, addonId: integration.addonId },
|
|
676
|
+
meta: { phase: 'create' },
|
|
677
|
+
})
|
|
598
678
|
|
|
599
679
|
const hasSettings = input.settings != null && Object.keys(input.settings).length > 0
|
|
600
680
|
if (!skipRestart && hasSettings) {
|
|
601
|
-
logger.info('settings present — restarting addon', {
|
|
681
|
+
logger.info('settings present — restarting addon', {
|
|
682
|
+
tags: { addonId: input.addonId },
|
|
683
|
+
meta: { phase: 'create' },
|
|
684
|
+
})
|
|
602
685
|
await ar.restartAddon(input.addonId)
|
|
603
|
-
logger.info('addon restart complete', {
|
|
686
|
+
logger.info('addon restart complete', {
|
|
687
|
+
tags: { addonId: input.addonId },
|
|
688
|
+
meta: { phase: 'create' },
|
|
689
|
+
})
|
|
604
690
|
} else {
|
|
605
|
-
logger.info('skipping restart (no settings or skipRestart=true)', {
|
|
691
|
+
logger.info('skipping restart (no settings or skipRestart=true)', {
|
|
692
|
+
meta: { phase: 'create' },
|
|
693
|
+
})
|
|
606
694
|
}
|
|
607
695
|
return integration
|
|
608
696
|
},
|
|
@@ -617,22 +705,21 @@ export function buildIntegrationsProvider(
|
|
|
617
705
|
const changedFields = Object.keys(updates).filter(
|
|
618
706
|
(k) => (updates as Record<string, unknown>)[k] !== undefined,
|
|
619
707
|
)
|
|
620
|
-
logger.info(
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
)
|
|
708
|
+
logger.info('request', {
|
|
709
|
+
tags: { integrationId: input.id, addonId: previous.addonId },
|
|
710
|
+
meta: { phase: 'update', fields: changedFields },
|
|
711
|
+
})
|
|
624
712
|
|
|
625
713
|
const result = await reg.updateIntegration(id, updates)
|
|
626
714
|
if (!result) throw new Error(`Integration "${id}" not found`)
|
|
627
715
|
|
|
628
|
-
const enabledChanged =
|
|
629
|
-
input.enabled !== undefined && input.enabled !== previous.enabled
|
|
716
|
+
const enabledChanged = input.enabled !== undefined && input.enabled !== previous.enabled
|
|
630
717
|
if (enabledChanged) {
|
|
631
718
|
const category = input.enabled ? 'integration.enabled' : 'integration.disabled'
|
|
632
|
-
logger.info(
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
)
|
|
719
|
+
logger.info('enabled state changed', {
|
|
720
|
+
tags: { integrationId: result.id, addonId: result.addonId },
|
|
721
|
+
meta: { phase: 'update', enabled: input.enabled },
|
|
722
|
+
})
|
|
636
723
|
eb.emit({
|
|
637
724
|
id: `integration-${category}-${Date.now()}`,
|
|
638
725
|
timestamp: new Date(),
|
|
@@ -646,17 +733,23 @@ export function buildIntegrationsProvider(
|
|
|
646
733
|
}
|
|
647
734
|
|
|
648
735
|
if (input.name !== undefined && input.name !== previous.name) {
|
|
649
|
-
logger.info(
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
)
|
|
736
|
+
logger.info('renamed', {
|
|
737
|
+
tags: { integrationId: result.id },
|
|
738
|
+
meta: { phase: 'update', previousName: previous.name, newName: input.name },
|
|
739
|
+
})
|
|
653
740
|
}
|
|
654
741
|
|
|
655
742
|
const infoChanged = input.info !== undefined
|
|
656
743
|
if (infoChanged) {
|
|
657
|
-
logger.info('info changed — restarting addon', {
|
|
744
|
+
logger.info('info changed — restarting addon', {
|
|
745
|
+
tags: { addonId: result.addonId },
|
|
746
|
+
meta: { phase: 'update' },
|
|
747
|
+
})
|
|
658
748
|
await ar.restartAddon(result.addonId)
|
|
659
|
-
logger.info('addon restart complete', {
|
|
749
|
+
logger.info('addon restart complete', {
|
|
750
|
+
tags: { addonId: result.addonId },
|
|
751
|
+
meta: { phase: 'update' },
|
|
752
|
+
})
|
|
660
753
|
} else {
|
|
661
754
|
logger.info('no restart needed (only enabled/name changed)', { meta: { phase: 'update' } })
|
|
662
755
|
}
|
|
@@ -671,21 +764,27 @@ export function buildIntegrationsProvider(
|
|
|
671
764
|
logger.warn('not found', { tags: { integrationId: input.id }, meta: { phase: 'delete' } })
|
|
672
765
|
throw new Error(`Integration "${input.id}" not found`)
|
|
673
766
|
}
|
|
674
|
-
logger.info(
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
)
|
|
767
|
+
logger.info('removing', {
|
|
768
|
+
tags: { integrationId: input.id, addonId: integration.addonId },
|
|
769
|
+
meta: { phase: 'delete', name: integration.name },
|
|
770
|
+
})
|
|
678
771
|
|
|
679
772
|
// Cascade-delete every live device whose integrationId matches.
|
|
680
773
|
// Best-effort: a device-removal hiccup must not abort the integration
|
|
681
774
|
// delete — log a warning and continue so the record + event always fire.
|
|
682
|
-
const dm =
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
775
|
+
const dm =
|
|
776
|
+
capabilityRegistry?.getSingleton<{
|
|
777
|
+
removeByIntegration?: (input: { integrationId: string }) => Promise<{ removed: number }>
|
|
778
|
+
listAll?: (input: Record<string, never>) => Promise<
|
|
779
|
+
readonly {
|
|
780
|
+
id: number
|
|
781
|
+
addonId: string
|
|
782
|
+
parentDeviceId: number | null
|
|
783
|
+
integrationId?: string
|
|
784
|
+
}[]
|
|
785
|
+
>
|
|
786
|
+
setIntegrationId?: (input: { deviceId: number; integrationId: string }) => Promise<void>
|
|
787
|
+
}>('device-manager') ?? null
|
|
689
788
|
|
|
690
789
|
// Claim legacy un-tagged devices BEFORE the cascade. Devices created
|
|
691
790
|
// before stamping (or whose provider never stamps, e.g. `provider-rtsp`)
|
|
@@ -711,7 +810,10 @@ export function buildIntegrationsProvider(
|
|
|
711
810
|
})),
|
|
712
811
|
)
|
|
713
812
|
for (const stamp of stamps) {
|
|
714
|
-
await dm.setIntegrationId({
|
|
813
|
+
await dm.setIntegrationId({
|
|
814
|
+
deviceId: stamp.deviceId,
|
|
815
|
+
integrationId: stamp.integrationId,
|
|
816
|
+
})
|
|
715
817
|
}
|
|
716
818
|
if (stamps.length > 0) {
|
|
717
819
|
logger.info('claimed legacy un-tagged devices for cascade', {
|
|
@@ -721,7 +823,8 @@ export function buildIntegrationsProvider(
|
|
|
721
823
|
}
|
|
722
824
|
} catch (err) {
|
|
723
825
|
logger.warn('legacy device claim failed (best-effort — continuing)', {
|
|
724
|
-
tags: { integrationId: input.id },
|
|
826
|
+
tags: { integrationId: input.id },
|
|
827
|
+
meta: { phase: 'delete', error: errMsg(err) },
|
|
725
828
|
})
|
|
726
829
|
}
|
|
727
830
|
}
|
|
@@ -729,21 +832,21 @@ export function buildIntegrationsProvider(
|
|
|
729
832
|
if (dm?.removeByIntegration) {
|
|
730
833
|
try {
|
|
731
834
|
const result = await dm.removeByIntegration({ integrationId: input.id })
|
|
732
|
-
logger.info(
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
)
|
|
835
|
+
logger.info('cascade-removed devices', {
|
|
836
|
+
tags: { integrationId: input.id },
|
|
837
|
+
meta: { phase: 'delete', removed: result.removed },
|
|
838
|
+
})
|
|
736
839
|
} catch (err) {
|
|
737
|
-
logger.warn(
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
)
|
|
840
|
+
logger.warn('device cascade-remove failed (best-effort — continuing)', {
|
|
841
|
+
tags: { integrationId: input.id },
|
|
842
|
+
meta: { phase: 'delete', error: errMsg(err) },
|
|
843
|
+
})
|
|
741
844
|
}
|
|
742
845
|
} else {
|
|
743
|
-
logger.warn(
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
)
|
|
846
|
+
logger.warn('device-manager not available — skipping cascade device removal', {
|
|
847
|
+
tags: { integrationId: input.id },
|
|
848
|
+
meta: { phase: 'delete' },
|
|
849
|
+
})
|
|
747
850
|
}
|
|
748
851
|
|
|
749
852
|
await reg.deleteIntegration(input.id)
|
|
@@ -752,13 +855,16 @@ export function buildIntegrationsProvider(
|
|
|
752
855
|
id: `integration-deleted-${Date.now()}`,
|
|
753
856
|
timestamp: new Date(),
|
|
754
857
|
source: { type: 'integration', id: input.id },
|
|
755
|
-
category:
|
|
858
|
+
category: EventCategory.IntegrationDeleted,
|
|
756
859
|
data: {
|
|
757
860
|
integrationId: input.id,
|
|
758
861
|
addonId: integration.addonId,
|
|
759
862
|
},
|
|
760
863
|
})
|
|
761
|
-
logger.info('completed (no restart)', {
|
|
864
|
+
logger.info('completed (no restart)', {
|
|
865
|
+
tags: { integrationId: input.id },
|
|
866
|
+
meta: { phase: 'delete' },
|
|
867
|
+
})
|
|
762
868
|
|
|
763
869
|
return { success: true, deletedId: input.id }
|
|
764
870
|
},
|
|
@@ -772,19 +878,28 @@ export function buildIntegrationsProvider(
|
|
|
772
878
|
const reg = requireIntegrationRegistry(ar)
|
|
773
879
|
const integration = await reg.getIntegration(input.id)
|
|
774
880
|
if (!integration) {
|
|
775
|
-
logger.warn('not found', {
|
|
881
|
+
logger.warn('not found', {
|
|
882
|
+
tags: { integrationId: input.id },
|
|
883
|
+
meta: { phase: 'setSettings' },
|
|
884
|
+
})
|
|
776
885
|
throw new Error(`Integration "${input.id}" not found`)
|
|
777
886
|
}
|
|
778
887
|
const settingsKeys = Object.keys(input.settings)
|
|
779
|
-
logger.info(
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
)
|
|
888
|
+
logger.info('request', {
|
|
889
|
+
tags: { integrationId: input.id, addonId: integration.addonId },
|
|
890
|
+
meta: { phase: 'setSettings', keys: settingsKeys },
|
|
891
|
+
})
|
|
783
892
|
await reg.setIntegrationSettings(input.id, input.settings)
|
|
784
893
|
|
|
785
|
-
logger.info('persisted — restarting addon', {
|
|
894
|
+
logger.info('persisted — restarting addon', {
|
|
895
|
+
tags: { addonId: integration.addonId },
|
|
896
|
+
meta: { phase: 'setSettings' },
|
|
897
|
+
})
|
|
786
898
|
await ar.restartAddon(integration.addonId)
|
|
787
|
-
logger.info('addon restart complete', {
|
|
899
|
+
logger.info('addon restart complete', {
|
|
900
|
+
tags: { addonId: integration.addonId },
|
|
901
|
+
meta: { phase: 'setSettings' },
|
|
902
|
+
})
|
|
788
903
|
return { success: true }
|
|
789
904
|
},
|
|
790
905
|
getAvailableTypes: async () => {
|
|
@@ -799,21 +914,22 @@ export function buildIntegrationsProvider(
|
|
|
799
914
|
// Markers that flag an addon as a creatable integration type live
|
|
800
915
|
// in the module-level `INTEGRATION_CAP_MARKERS` set (exported so the
|
|
801
916
|
// integration-markers spec can assert the recognised caps).
|
|
802
|
-
const providerAddons = addons.filter(
|
|
803
|
-
a
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
917
|
+
const providerAddons = addons.filter(
|
|
918
|
+
(a) =>
|
|
919
|
+
a.process?.state !== 'failed' &&
|
|
920
|
+
a.manifest.capabilities?.some((c) => {
|
|
921
|
+
const name = typeof c === 'string' ? c : c.name
|
|
922
|
+
return typeof name === 'string' && INTEGRATION_CAP_MARKERS.has(name)
|
|
923
|
+
}),
|
|
808
924
|
)
|
|
809
925
|
const integrations = await reg.listIntegrations()
|
|
810
|
-
return providerAddons.map(addon => {
|
|
926
|
+
return providerAddons.map((addon) => {
|
|
811
927
|
const m = addon.manifest
|
|
812
928
|
const d = addon.declaration
|
|
813
929
|
const icon = d?.icon ?? m.icon
|
|
814
930
|
const color = d?.color ?? m.color ?? '#78716c'
|
|
815
931
|
const instanceMode = d?.instanceMode ?? m.instanceMode ?? 'multiple'
|
|
816
|
-
const existing = integrations.filter(i => i.addonId === m.id)
|
|
932
|
+
const existing = integrations.filter((i) => i.addonId === m.id)
|
|
817
933
|
const provider = getDeviceProvider(ar, m.id)
|
|
818
934
|
const discoveryMode = provider?.discoveryMode ?? 'manual'
|
|
819
935
|
|
|
@@ -824,20 +940,21 @@ export function buildIntegrationsProvider(
|
|
|
824
940
|
// marker wins when both are present (an integration-style addon may
|
|
825
941
|
// also expose a `device-provider` shim); the broker step is the
|
|
826
942
|
// intended entry point for it.
|
|
827
|
-
const capNames = (m.capabilities ?? []).map(c => (typeof c === 'string' ? c : c.name))
|
|
828
|
-
const kind: 'device-adoption' | 'device-provider' =
|
|
829
|
-
|
|
943
|
+
const capNames = (m.capabilities ?? []).map((c) => (typeof c === 'string' ? c : c.name))
|
|
944
|
+
const kind: 'device-adoption' | 'device-provider' = capNames.includes('device-adoption')
|
|
945
|
+
? 'device-adoption'
|
|
946
|
+
: 'device-provider'
|
|
830
947
|
|
|
831
948
|
// For device-adoption addons, the broker kind to create/link comes
|
|
832
949
|
// from the addon manifest (`brokerKind`). Null for device-provider
|
|
833
950
|
// addons, which carry no broker.
|
|
834
|
-
const brokerKind =
|
|
835
|
-
? (d?.brokerKind ?? m.brokerKind ?? null)
|
|
836
|
-
: null
|
|
951
|
+
const brokerKind =
|
|
952
|
+
kind === 'device-adoption' ? (d?.brokerKind ?? m.brokerKind ?? null) : null
|
|
837
953
|
|
|
838
|
-
const supportsLocationImport =
|
|
839
|
-
|
|
840
|
-
|
|
954
|
+
const supportsLocationImport =
|
|
955
|
+
kind === 'device-adoption'
|
|
956
|
+
? (d?.supportsLocationImport ?? m.supportsLocationImport ?? false)
|
|
957
|
+
: false
|
|
841
958
|
|
|
842
959
|
return {
|
|
843
960
|
addonId: m.id,
|
|
@@ -850,7 +967,7 @@ export function buildIntegrationsProvider(
|
|
|
850
967
|
kind,
|
|
851
968
|
brokerKind,
|
|
852
969
|
supportsLocationImport,
|
|
853
|
-
existingInstances: existing.map(i => ({
|
|
970
|
+
existingInstances: existing.map((i) => ({
|
|
854
971
|
id: i.id,
|
|
855
972
|
name: i.name,
|
|
856
973
|
})),
|
|
@@ -871,29 +988,43 @@ export function buildIntegrationsProvider(
|
|
|
871
988
|
const registry = ar.getCapabilityRegistry()
|
|
872
989
|
const brokerId = input.settings['brokerId']
|
|
873
990
|
if (typeof brokerId === 'string' && brokerId.length > 0) {
|
|
874
|
-
const brokerProvider = registry.getProviderByAddonId<IBrokerProvider>(
|
|
991
|
+
const brokerProvider = registry.getProviderByAddonId<IBrokerProvider>(
|
|
992
|
+
'broker',
|
|
993
|
+
input.addonId,
|
|
994
|
+
)
|
|
875
995
|
if (!brokerProvider) {
|
|
876
|
-
return {
|
|
996
|
+
return {
|
|
997
|
+
success: false,
|
|
998
|
+
error: `Broker provider for addon '${input.addonId}' is not available`,
|
|
999
|
+
}
|
|
877
1000
|
}
|
|
878
1001
|
try {
|
|
879
1002
|
const result = await brokerProvider.testConnection({ id: brokerId })
|
|
880
|
-
return result.ok
|
|
881
|
-
? { success: true }
|
|
882
|
-
: { success: false, error: result.error }
|
|
1003
|
+
return result.ok ? { success: true } : { success: false, error: result.error }
|
|
883
1004
|
} catch (err) {
|
|
884
1005
|
return { success: false, error: errMsg(err) }
|
|
885
1006
|
}
|
|
886
1007
|
}
|
|
887
1008
|
|
|
888
1009
|
// Default — RTSP/Frigate/ONVIF legacy path: probe the stream URL.
|
|
889
|
-
const url = String(
|
|
890
|
-
input.settings['main_stream_url'] ?? input.settings['url'] ?? '',
|
|
891
|
-
).trim()
|
|
1010
|
+
const url = String(input.settings['main_stream_url'] ?? input.settings['url'] ?? '').trim()
|
|
892
1011
|
if (!url) return { success: false, error: 'No stream URL provided' }
|
|
893
1012
|
try {
|
|
894
1013
|
const { stdout } = await execFileAsync(
|
|
895
1014
|
'ffprobe',
|
|
896
|
-
[
|
|
1015
|
+
[
|
|
1016
|
+
'-v',
|
|
1017
|
+
'error',
|
|
1018
|
+
'-rtsp_transport',
|
|
1019
|
+
'tcp',
|
|
1020
|
+
'-timeout',
|
|
1021
|
+
'3000000',
|
|
1022
|
+
'-show_entries',
|
|
1023
|
+
'stream=codec_name,width,height',
|
|
1024
|
+
'-of',
|
|
1025
|
+
'json',
|
|
1026
|
+
url,
|
|
1027
|
+
],
|
|
897
1028
|
{ timeout: 5000 },
|
|
898
1029
|
)
|
|
899
1030
|
const parsed = asJsonObject(JSON.parse(stdout))
|
|
@@ -988,7 +1119,9 @@ export function buildAddonsProvider(
|
|
|
988
1119
|
|
|
989
1120
|
const bulkCoordinator = new BulkUpdateCoordinator({
|
|
990
1121
|
eventBus: bulkEventBus,
|
|
991
|
-
updateAddon: async (i) => {
|
|
1122
|
+
updateAddon: async (i) => {
|
|
1123
|
+
await ps.updatePackage(i.name, i.version)
|
|
1124
|
+
},
|
|
992
1125
|
updateFrameworkPackage: async (i) => {
|
|
993
1126
|
await ps.updateFrameworkPackage({
|
|
994
1127
|
packageName: i.packageName,
|
|
@@ -1007,7 +1140,10 @@ export function buildAddonsProvider(
|
|
|
1007
1140
|
return {
|
|
1008
1141
|
list: async () => {
|
|
1009
1142
|
const rollbackable = ps.getRollbackablePackages()
|
|
1010
|
-
const healthByPackage = new Map<
|
|
1143
|
+
const healthByPackage = new Map<
|
|
1144
|
+
string,
|
|
1145
|
+
ReturnType<typeof ar.getAddonHealthSnapshot>[number]
|
|
1146
|
+
>()
|
|
1011
1147
|
for (const h of ar.getAddonHealthSnapshot()) {
|
|
1012
1148
|
healthByPackage.set(h.packageName, h)
|
|
1013
1149
|
}
|
|
@@ -1017,11 +1153,12 @@ export function buildAddonsProvider(
|
|
|
1017
1153
|
health: healthByPackage.get(item.manifest.packageName) ?? null,
|
|
1018
1154
|
}))
|
|
1019
1155
|
},
|
|
1020
|
-
getLogs: async (input) =>
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1156
|
+
getLogs: async (input) =>
|
|
1157
|
+
ls.query({
|
|
1158
|
+
tags: { addonId: input.addonId },
|
|
1159
|
+
limit: input.limit,
|
|
1160
|
+
level: input.level,
|
|
1161
|
+
}),
|
|
1025
1162
|
listPackages: async () => ps.listInstalled(),
|
|
1026
1163
|
installPackage: async (input) => ps.installAndLoad(input.packageName, input.version),
|
|
1027
1164
|
installFromWorkspace: async (input) => ps.installFromWorkspaceAndLoad(input.packageName),
|
|
@@ -1039,10 +1176,11 @@ export function buildAddonsProvider(
|
|
|
1039
1176
|
},
|
|
1040
1177
|
listUpdates: async (input) => {
|
|
1041
1178
|
const nodeId = input.nodeId
|
|
1042
|
-
const updates =
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1179
|
+
const updates =
|
|
1180
|
+
nodeId === undefined || isHubNode(nodeId)
|
|
1181
|
+
? await ps.checkUpdates()
|
|
1182
|
+
: await ps.checkUpdatesForInstalled(await fetchAgentInstalledPackages(broker, nodeId))
|
|
1183
|
+
return updates.map((u) => ({ ...u, isSystem: frameworkAllowSet.has(u.name) }))
|
|
1046
1184
|
},
|
|
1047
1185
|
updatePackage: async (input) => {
|
|
1048
1186
|
const nodeId = input.nodeId
|
|
@@ -1052,7 +1190,11 @@ export function buildAddonsProvider(
|
|
|
1052
1190
|
// Agent target: the hub packs the resolved version and ships the
|
|
1053
1191
|
// tarball over `$agent.deploy` — the agent has no npm runtime.
|
|
1054
1192
|
const packed = await ps.packPackage(input.name, input.version)
|
|
1055
|
-
await broker.call(
|
|
1193
|
+
await broker.call(
|
|
1194
|
+
'$agent.deploy',
|
|
1195
|
+
{ addonId: input.name, bundle: packed.buffer },
|
|
1196
|
+
{ nodeID: nodeId, timeout: 120_000 },
|
|
1197
|
+
)
|
|
1056
1198
|
await broker.call('$agent.reload', {}, { nodeID: nodeId, timeout: 120_000 })
|
|
1057
1199
|
return { success: true, name: input.name, version: packed.version, nodeId }
|
|
1058
1200
|
},
|
|
@@ -1078,14 +1220,12 @@ export function buildAddonsProvider(
|
|
|
1078
1220
|
const caps = registry.listCapabilities()
|
|
1079
1221
|
const found = caps.find((c) => c.name === input.capName)
|
|
1080
1222
|
if (!found) return []
|
|
1081
|
-
const mode = found.mode === 'collection' ? 'collection' as const : 'singleton' as const
|
|
1223
|
+
const mode = found.mode === 'collection' ? ('collection' as const) : ('singleton' as const)
|
|
1082
1224
|
const disabled = new Set(found.disabledProviders)
|
|
1083
1225
|
return found.providers.map((addonId) => ({
|
|
1084
1226
|
addonId,
|
|
1085
1227
|
mode,
|
|
1086
|
-
isActive: mode === 'collection'
|
|
1087
|
-
? !disabled.has(addonId)
|
|
1088
|
-
: found.activeProvider === addonId,
|
|
1228
|
+
isActive: mode === 'collection' ? !disabled.has(addonId) : found.activeProvider === addonId,
|
|
1089
1229
|
}))
|
|
1090
1230
|
},
|
|
1091
1231
|
setCapabilityProviderEnabled: async (input) => {
|
|
@@ -1119,23 +1259,20 @@ export function buildAddonsProvider(
|
|
|
1119
1259
|
// Reuses the same `capabilities.collection.<cap>` key/format the
|
|
1120
1260
|
// `capabilities` core router writes — via the shared canonical writer.
|
|
1121
1261
|
const updated = registry.listCapabilities().find((c) => c.name === input.capName)
|
|
1122
|
-
persistCollectionDisabled(
|
|
1123
|
-
configService,
|
|
1124
|
-
input.capName,
|
|
1125
|
-
updated?.disabledProviders ?? [],
|
|
1126
|
-
)
|
|
1262
|
+
persistCollectionDisabled(configService, input.capName, updated?.disabledProviders ?? [])
|
|
1127
1263
|
return { success: true as const }
|
|
1128
1264
|
},
|
|
1129
|
-
updateFrameworkPackage: async (input) =>
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1265
|
+
updateFrameworkPackage: async (input) =>
|
|
1266
|
+
ps.updateFrameworkPackage({
|
|
1267
|
+
packageName: input.packageName,
|
|
1268
|
+
...(input.version !== undefined ? { version: input.version } : {}),
|
|
1269
|
+
...(ctx.user?.username !== undefined
|
|
1270
|
+
? { requestedBy: ctx.user.username }
|
|
1271
|
+
: ctx.user?.id !== undefined
|
|
1272
|
+
? { requestedBy: ctx.user.id }
|
|
1273
|
+
: {}),
|
|
1274
|
+
...(input.deferRestart !== undefined ? { deferRestart: input.deferRestart } : {}),
|
|
1275
|
+
}),
|
|
1139
1276
|
getVersions: async (input) => ps.getPackageVersions(input.name),
|
|
1140
1277
|
restartAddon: async (input) => ar.restartAddon(input.addonId),
|
|
1141
1278
|
retryLoad: async (input) => {
|
|
@@ -1143,7 +1280,8 @@ export function buildAddonsProvider(
|
|
|
1143
1280
|
return { success: true as const }
|
|
1144
1281
|
},
|
|
1145
1282
|
getAutoUpdateSettings: async () => ps.getAutoUpdateSettings(),
|
|
1146
|
-
setAutoUpdateSettings: async (input) =>
|
|
1283
|
+
setAutoUpdateSettings: async (input) =>
|
|
1284
|
+
ps.setAutoUpdateSettings(input.channel, input.intervalSeconds),
|
|
1147
1285
|
getAddonAutoUpdate: async (input) => ps.getAddonAutoUpdate(input.addonId),
|
|
1148
1286
|
setAddonAutoUpdate: async (input) => ps.setAddonAutoUpdate(input.addonId, input.channel),
|
|
1149
1287
|
applyAutoUpdateToAll: async (input) => {
|
|
@@ -1174,11 +1312,17 @@ export function buildAddonsProvider(
|
|
|
1174
1312
|
onAddonLogs: (input, push) => {
|
|
1175
1313
|
const unsubscribe = ls.subscribe(
|
|
1176
1314
|
{ tags: { addonId: input.addonId }, level: input.level },
|
|
1177
|
-
(entry: {
|
|
1315
|
+
(entry: {
|
|
1316
|
+
timestamp: Date | string | number
|
|
1317
|
+
level: string
|
|
1318
|
+
message: string
|
|
1319
|
+
scope?: string
|
|
1320
|
+
}) => {
|
|
1178
1321
|
push({
|
|
1179
|
-
timestamp:
|
|
1180
|
-
|
|
1181
|
-
|
|
1322
|
+
timestamp:
|
|
1323
|
+
entry.timestamp instanceof Date
|
|
1324
|
+
? entry.timestamp.toISOString()
|
|
1325
|
+
: String(entry.timestamp),
|
|
1182
1326
|
level: entry.level,
|
|
1183
1327
|
message: entry.message,
|
|
1184
1328
|
scope: entry.scope,
|