@camstack/system 1.0.6 → 1.0.8

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.
Files changed (33) hide show
  1. package/dist/addon-runner.js +40 -23
  2. package/dist/addon-runner.mjs +20 -4
  3. package/dist/addon-utils.d.ts +20 -0
  4. package/dist/addon-utils.js +11 -0
  5. package/dist/addon-utils.mjs +3 -0
  6. package/dist/builtins/device-manager/device-manager.addon.js +8 -8
  7. package/dist/builtins/device-manager/device-manager.addon.mjs +8 -8
  8. package/dist/builtins/native-metrics/native-metrics.addon.d.ts +8 -0
  9. package/dist/builtins/native-metrics/native-metrics.addon.js +50 -3
  10. package/dist/builtins/native-metrics/native-metrics.addon.mjs +50 -3
  11. package/dist/builtins/platform-probe/index.js +27 -139
  12. package/dist/builtins/platform-probe/index.mjs +28 -140
  13. package/dist/builtins/platform-probe/platform-scorer.d.ts +17 -10
  14. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +2 -2
  15. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +2 -2
  16. package/dist/custom-action-registry-BEXwC-oo.mjs +38 -0
  17. package/dist/custom-action-registry-vLYEFTtv.js +43 -0
  18. package/dist/index.js +129 -779
  19. package/dist/index.mjs +100 -750
  20. package/dist/kernel/config-manager.d.ts +4 -4
  21. package/dist/kernel/fs-utils.d.ts +16 -6
  22. package/dist/kernel/index.d.ts +1 -1
  23. package/dist/kernel/moleculer/device-cap-proxy.d.ts +2 -1
  24. package/dist/kernel/moleculer/readiness-context.d.ts +2 -1
  25. package/dist/kernel/transport/child-cap-protocol.d.ts +10 -0
  26. package/dist/{manifest-python-deps-B4BmMoGT.js → manifest-python-deps-BWURo7dc.js} +62 -88
  27. package/dist/{manifest-python-deps-CXbKrOdk.mjs → manifest-python-deps-BcrTzHH_.mjs} +55 -75
  28. package/dist/model-download-service-C7AjBsX9.mjs +668 -0
  29. package/dist/model-download-service-JtVQtbb6.js +752 -0
  30. package/dist/process/resource-monitor.d.ts +9 -0
  31. package/dist/{resource-monitor-ClDGFyf6.mjs → resource-monitor-BkP504Vq.mjs} +20 -1
  32. package/dist/{resource-monitor-IIEanuJt.js → resource-monitor-DNNomR-i.js} +21 -1
  33. package/package.json +6 -1
@@ -60,8 +60,8 @@ async function getAvailableRAM_MB() {
60
60
  const match = readFileSync("/proc/meminfo", "utf8").match(/MemAvailable:\s+(\d+)\s+kB/);
61
61
  if (match) return Math.round(parseInt(match[1]) / 1024);
62
62
  }
63
- } catch (err) {
64
- console.debug(`RAM probe failed, using total RAM fallback: ${(0, _camstack_types.errMsg)(err)}`);
63
+ } catch (probeErr) {
64
+ console.debug(`RAM probe failed, using total RAM fallback: ${(0, _camstack_types.errMsg)(probeErr)}`);
65
65
  }
66
66
  return Math.round(node_os.totalmem() / 1024 / 1024);
67
67
  }
@@ -73,10 +73,9 @@ var PlatformScorer = class {
73
73
  * Path to the embedded portable Python (from `ctx.deps.ensurePython()`).
74
74
  * The Docker hub ships NO system `python3` — inference runs on the
75
75
  * downloaded portable build — so probing system `python3`/`python` would
76
- * wrongly report "Python not found" and never surface coreml/openvino
77
- * backends. When set, the Python module probes run against THIS binary;
78
- * `null` falls back to a system `python3`/`python` lookup (dev / agent
79
- * environments where Python is on PATH).
76
+ * wrongly report "Python not found". When set, the interpreter lookup
77
+ * tries THIS binary first; `null` falls back to a system `python3`/`python`
78
+ * lookup (dev / agent environments where Python is on PATH).
80
79
  */
81
80
  embeddedPythonPath;
82
81
  constructor(logger = noopLogger, embeddedPythonPath = null) {
@@ -84,14 +83,16 @@ var PlatformScorer = class {
84
83
  this.embeddedPythonPath = embeddedPythonPath;
85
84
  }
86
85
  /**
87
- * Probe hardware + runtimes and score all backend combos.
86
+ * Probe hardware + derive scores from pure rules and score all backend combos.
88
87
  *
89
88
  * An optional `onPhase` callback is invoked at each step so consumers
90
89
  * (e.g. the platform-probe addon) can emit live progress events on
91
90
  * the event bus. The callback takes a phase id + a typed payload; all
92
- * phases fire in strict order: `started` → `hardware` → `node-backends`
93
- * `python-backends` `scored` `done`. On failure, `error` fires
94
- * once with the exception. Cached after first call.
91
+ * phases fire in strict order: `started` → `hardware` → `scored` → `done`.
92
+ * On failure, `error` fires once with the exception. Cached after first call.
93
+ *
94
+ * Scores are hardware-driven only (no Python module import subprocess):
95
+ * `scoreRuntimes` from `@camstack/types` is the single source of truth.
95
96
  */
96
97
  async probe(onPhase) {
97
98
  if (this.cached) return this.cached;
@@ -109,19 +110,10 @@ var PlatformScorer = class {
109
110
  if (hardware.gpu) this.logger.info("GPU detected", { meta: { name: hardware.gpu.name } });
110
111
  if (hardware.npu) this.logger.info("NPU detected", { meta: { type: hardware.npu.type } });
111
112
  onPhase?.("hardware", { hardware });
112
- this.logger.info("Probing Python backends...");
113
- const pythonInfo = await this.probePythonBackends();
114
- if (pythonInfo.pythonPath) this.logger.info("Python backends detected", { meta: {
115
- pythonPath: pythonInfo.pythonPath,
116
- backends: pythonInfo.backends.filter((b) => b.available).map((b) => b.id)
117
- } });
113
+ const { scores, best: bestScore } = (0, _camstack_types.scoreRuntimes)(hardware);
114
+ const pythonPath = await this.resolvePythonPath();
115
+ if (pythonPath) this.logger.info("Python interpreter located", { meta: { pythonPath } });
118
116
  else this.logger.info("Python: not found");
119
- onPhase?.("python-backends", {
120
- pythonPath: pythonInfo.pythonPath,
121
- backends: pythonInfo.backends
122
- });
123
- const scores = this.scoreBackends(hardware, pythonInfo.backends);
124
- const bestScore = scores.find((s) => s.available) ?? scores[scores.length - 1];
125
117
  const elapsed = Date.now() - start;
126
118
  this.logger.info("Scoring complete", { meta: {
127
119
  elapsedMs: elapsed,
@@ -144,9 +136,9 @@ var PlatformScorer = class {
144
136
  } });
145
137
  this.cached = {
146
138
  hardware,
147
- scores,
139
+ scores: [...scores],
148
140
  bestScore,
149
- pythonPath: pythonInfo.pythonPath
141
+ pythonPath
150
142
  };
151
143
  onPhase?.("scored", {
152
144
  scores,
@@ -231,130 +223,26 @@ var PlatformScorer = class {
231
223
  npu
232
224
  };
233
225
  }
234
- async probePythonBackends() {
226
+ /**
227
+ * Locates a Python interpreter without importing any module.
228
+ *
229
+ * Tries: embedded path (if configured) → `python3` → `python`.
230
+ * Returns the first executable found, or `null` if none respond.
231
+ * MUST NOT run `-c "import ..."` — interpreter location only.
232
+ */
233
+ async resolvePythonPath() {
235
234
  const candidates = [
236
235
  ...this.embeddedPythonPath ? [this.embeddedPythonPath] : [],
237
236
  "python3",
238
237
  "python"
239
238
  ];
240
- let pythonPath = null;
241
239
  for (const cmd of candidates) try {
242
240
  await execFileAsync$2(cmd, ["--version"], { timeout: 5e3 });
243
- pythonPath = cmd;
244
- break;
241
+ return cmd;
245
242
  } catch (err) {
246
- console.debug(`Python command "${cmd}" not found: ${(0, _camstack_types.errMsg)(err)}`);
243
+ this.logger.debug(`Python command "${cmd}" not found`, { meta: { error: (0, _camstack_types.errMsg)(err) } });
247
244
  }
248
- if (!pythonPath) return {
249
- pythonPath: null,
250
- backends: []
251
- };
252
- const results = await Promise.all([
253
- [
254
- "coremltools",
255
- "coreml",
256
- "coreml"
257
- ],
258
- [
259
- "openvino.runtime",
260
- "openvino",
261
- "openvino"
262
- ],
263
- [
264
- "torch",
265
- "pytorch",
266
- "onnx"
267
- ],
268
- [
269
- "onnxruntime",
270
- "onnx-py",
271
- "onnx"
272
- ]
273
- ].map(async ([mod, id, format]) => {
274
- const probeStart = Date.now();
275
- let available = false;
276
- try {
277
- await execFileAsync$2(pythonPath, ["-c", `import ${mod}`], { timeout: 3e4 });
278
- available = true;
279
- this.logger.info("Python backend confirmed", { meta: {
280
- backend: id,
281
- module: mod
282
- } });
283
- } catch (err) {
284
- console.debug(`Python module "${mod}" not installed: ${(0, _camstack_types.errMsg)(err)}`);
285
- if (id === "openvino") this.logger.info("Python backend not yet confirmed: openvino (may be pending proactive install on Intel hardware — falling back to onnx-cpu)", { meta: { module: mod } });
286
- }
287
- const probeMs = Date.now() - probeStart;
288
- this.logger.debug("Python module probed", { meta: {
289
- module: mod,
290
- available,
291
- probeMs
292
- } });
293
- return {
294
- mod,
295
- id,
296
- format,
297
- available
298
- };
299
- }));
300
- const backends = [];
301
- for (const r of results) if (r.id === "coreml" && node_os.platform() === "darwin") backends.push({
302
- id: r.id,
303
- format: r.format,
304
- available: r.available
305
- });
306
- else if (r.available) backends.push({
307
- id: r.id,
308
- format: r.format,
309
- available: r.available
310
- });
311
- return {
312
- pythonPath,
313
- backends
314
- };
315
- }
316
- scoreBackends(hardware, pythonBackends) {
317
- const scores = [];
318
- if (hardware.platform === "darwin" && hardware.arch === "arm64") {
319
- const pyCoreMl = pythonBackends.find((b) => b.id === "coreml");
320
- if (pyCoreMl) scores.push({
321
- runtime: "python",
322
- backend: "coreml",
323
- format: "coreml",
324
- score: 95,
325
- reason: "Apple Neural Engine (Python CoreML)",
326
- available: pyCoreMl.available
327
- });
328
- }
329
- if (hardware.gpu?.type === "nvidia") scores.push({
330
- runtime: "python",
331
- backend: "cuda",
332
- format: "onnx",
333
- score: 85,
334
- reason: "NVIDIA CUDA (Python ONNX Runtime)",
335
- available: true
336
- });
337
- const openvino = pythonBackends.find((b) => b.id === "openvino");
338
- if (openvino) {
339
- const score = hardware.npu?.type === "intel-npu" ? 90 : 80;
340
- scores.push({
341
- runtime: "python",
342
- backend: "openvino",
343
- format: "openvino",
344
- score,
345
- reason: "Intel OpenVINO",
346
- available: openvino.available
347
- });
348
- }
349
- scores.push({
350
- runtime: "python",
351
- backend: "cpu",
352
- format: "onnx",
353
- score: 50,
354
- reason: "CPU (Python ONNX Runtime)",
355
- available: true
356
- });
357
- return scores.toSorted((a, b) => b.score - a.score);
245
+ return null;
358
246
  }
359
247
  };
360
248
  //#endregion
@@ -1,5 +1,5 @@
1
1
  import * as fs from "node:fs";
2
- import { BaseAddon, EventCategory, errMsg, platformProbeCapability } from "@camstack/types";
2
+ import { BaseAddon, EventCategory, errMsg, platformProbeCapability, scoreRuntimes } from "@camstack/types";
3
3
  import { execFile } from "node:child_process";
4
4
  import { promisify } from "node:util";
5
5
  import * as os from "node:os";
@@ -53,8 +53,8 @@ async function getAvailableRAM_MB() {
53
53
  const match = readFileSync("/proc/meminfo", "utf8").match(/MemAvailable:\s+(\d+)\s+kB/);
54
54
  if (match) return Math.round(parseInt(match[1]) / 1024);
55
55
  }
56
- } catch (err) {
57
- console.debug(`RAM probe failed, using total RAM fallback: ${errMsg(err)}`);
56
+ } catch (probeErr) {
57
+ console.debug(`RAM probe failed, using total RAM fallback: ${errMsg(probeErr)}`);
58
58
  }
59
59
  return Math.round(os.totalmem() / 1024 / 1024);
60
60
  }
@@ -66,10 +66,9 @@ var PlatformScorer = class {
66
66
  * Path to the embedded portable Python (from `ctx.deps.ensurePython()`).
67
67
  * The Docker hub ships NO system `python3` — inference runs on the
68
68
  * downloaded portable build — so probing system `python3`/`python` would
69
- * wrongly report "Python not found" and never surface coreml/openvino
70
- * backends. When set, the Python module probes run against THIS binary;
71
- * `null` falls back to a system `python3`/`python` lookup (dev / agent
72
- * environments where Python is on PATH).
69
+ * wrongly report "Python not found". When set, the interpreter lookup
70
+ * tries THIS binary first; `null` falls back to a system `python3`/`python`
71
+ * lookup (dev / agent environments where Python is on PATH).
73
72
  */
74
73
  embeddedPythonPath;
75
74
  constructor(logger = noopLogger, embeddedPythonPath = null) {
@@ -77,14 +76,16 @@ var PlatformScorer = class {
77
76
  this.embeddedPythonPath = embeddedPythonPath;
78
77
  }
79
78
  /**
80
- * Probe hardware + runtimes and score all backend combos.
79
+ * Probe hardware + derive scores from pure rules and score all backend combos.
81
80
  *
82
81
  * An optional `onPhase` callback is invoked at each step so consumers
83
82
  * (e.g. the platform-probe addon) can emit live progress events on
84
83
  * the event bus. The callback takes a phase id + a typed payload; all
85
- * phases fire in strict order: `started` → `hardware` → `node-backends`
86
- * `python-backends` `scored` `done`. On failure, `error` fires
87
- * once with the exception. Cached after first call.
84
+ * phases fire in strict order: `started` → `hardware` → `scored` → `done`.
85
+ * On failure, `error` fires once with the exception. Cached after first call.
86
+ *
87
+ * Scores are hardware-driven only (no Python module import subprocess):
88
+ * `scoreRuntimes` from `@camstack/types` is the single source of truth.
88
89
  */
89
90
  async probe(onPhase) {
90
91
  if (this.cached) return this.cached;
@@ -102,19 +103,10 @@ var PlatformScorer = class {
102
103
  if (hardware.gpu) this.logger.info("GPU detected", { meta: { name: hardware.gpu.name } });
103
104
  if (hardware.npu) this.logger.info("NPU detected", { meta: { type: hardware.npu.type } });
104
105
  onPhase?.("hardware", { hardware });
105
- this.logger.info("Probing Python backends...");
106
- const pythonInfo = await this.probePythonBackends();
107
- if (pythonInfo.pythonPath) this.logger.info("Python backends detected", { meta: {
108
- pythonPath: pythonInfo.pythonPath,
109
- backends: pythonInfo.backends.filter((b) => b.available).map((b) => b.id)
110
- } });
106
+ const { scores, best: bestScore } = scoreRuntimes(hardware);
107
+ const pythonPath = await this.resolvePythonPath();
108
+ if (pythonPath) this.logger.info("Python interpreter located", { meta: { pythonPath } });
111
109
  else this.logger.info("Python: not found");
112
- onPhase?.("python-backends", {
113
- pythonPath: pythonInfo.pythonPath,
114
- backends: pythonInfo.backends
115
- });
116
- const scores = this.scoreBackends(hardware, pythonInfo.backends);
117
- const bestScore = scores.find((s) => s.available) ?? scores[scores.length - 1];
118
110
  const elapsed = Date.now() - start;
119
111
  this.logger.info("Scoring complete", { meta: {
120
112
  elapsedMs: elapsed,
@@ -137,9 +129,9 @@ var PlatformScorer = class {
137
129
  } });
138
130
  this.cached = {
139
131
  hardware,
140
- scores,
132
+ scores: [...scores],
141
133
  bestScore,
142
- pythonPath: pythonInfo.pythonPath
134
+ pythonPath
143
135
  };
144
136
  onPhase?.("scored", {
145
137
  scores,
@@ -224,130 +216,26 @@ var PlatformScorer = class {
224
216
  npu
225
217
  };
226
218
  }
227
- async probePythonBackends() {
219
+ /**
220
+ * Locates a Python interpreter without importing any module.
221
+ *
222
+ * Tries: embedded path (if configured) → `python3` → `python`.
223
+ * Returns the first executable found, or `null` if none respond.
224
+ * MUST NOT run `-c "import ..."` — interpreter location only.
225
+ */
226
+ async resolvePythonPath() {
228
227
  const candidates = [
229
228
  ...this.embeddedPythonPath ? [this.embeddedPythonPath] : [],
230
229
  "python3",
231
230
  "python"
232
231
  ];
233
- let pythonPath = null;
234
232
  for (const cmd of candidates) try {
235
233
  await execFileAsync$2(cmd, ["--version"], { timeout: 5e3 });
236
- pythonPath = cmd;
237
- break;
234
+ return cmd;
238
235
  } catch (err) {
239
- console.debug(`Python command "${cmd}" not found: ${errMsg(err)}`);
236
+ this.logger.debug(`Python command "${cmd}" not found`, { meta: { error: errMsg(err) } });
240
237
  }
241
- if (!pythonPath) return {
242
- pythonPath: null,
243
- backends: []
244
- };
245
- const results = await Promise.all([
246
- [
247
- "coremltools",
248
- "coreml",
249
- "coreml"
250
- ],
251
- [
252
- "openvino.runtime",
253
- "openvino",
254
- "openvino"
255
- ],
256
- [
257
- "torch",
258
- "pytorch",
259
- "onnx"
260
- ],
261
- [
262
- "onnxruntime",
263
- "onnx-py",
264
- "onnx"
265
- ]
266
- ].map(async ([mod, id, format]) => {
267
- const probeStart = Date.now();
268
- let available = false;
269
- try {
270
- await execFileAsync$2(pythonPath, ["-c", `import ${mod}`], { timeout: 3e4 });
271
- available = true;
272
- this.logger.info("Python backend confirmed", { meta: {
273
- backend: id,
274
- module: mod
275
- } });
276
- } catch (err) {
277
- console.debug(`Python module "${mod}" not installed: ${errMsg(err)}`);
278
- if (id === "openvino") this.logger.info("Python backend not yet confirmed: openvino (may be pending proactive install on Intel hardware — falling back to onnx-cpu)", { meta: { module: mod } });
279
- }
280
- const probeMs = Date.now() - probeStart;
281
- this.logger.debug("Python module probed", { meta: {
282
- module: mod,
283
- available,
284
- probeMs
285
- } });
286
- return {
287
- mod,
288
- id,
289
- format,
290
- available
291
- };
292
- }));
293
- const backends = [];
294
- for (const r of results) if (r.id === "coreml" && os.platform() === "darwin") backends.push({
295
- id: r.id,
296
- format: r.format,
297
- available: r.available
298
- });
299
- else if (r.available) backends.push({
300
- id: r.id,
301
- format: r.format,
302
- available: r.available
303
- });
304
- return {
305
- pythonPath,
306
- backends
307
- };
308
- }
309
- scoreBackends(hardware, pythonBackends) {
310
- const scores = [];
311
- if (hardware.platform === "darwin" && hardware.arch === "arm64") {
312
- const pyCoreMl = pythonBackends.find((b) => b.id === "coreml");
313
- if (pyCoreMl) scores.push({
314
- runtime: "python",
315
- backend: "coreml",
316
- format: "coreml",
317
- score: 95,
318
- reason: "Apple Neural Engine (Python CoreML)",
319
- available: pyCoreMl.available
320
- });
321
- }
322
- if (hardware.gpu?.type === "nvidia") scores.push({
323
- runtime: "python",
324
- backend: "cuda",
325
- format: "onnx",
326
- score: 85,
327
- reason: "NVIDIA CUDA (Python ONNX Runtime)",
328
- available: true
329
- });
330
- const openvino = pythonBackends.find((b) => b.id === "openvino");
331
- if (openvino) {
332
- const score = hardware.npu?.type === "intel-npu" ? 90 : 80;
333
- scores.push({
334
- runtime: "python",
335
- backend: "openvino",
336
- format: "openvino",
337
- score,
338
- reason: "Intel OpenVINO",
339
- available: openvino.available
340
- });
341
- }
342
- scores.push({
343
- runtime: "python",
344
- backend: "cpu",
345
- format: "onnx",
346
- score: 50,
347
- reason: "CPU (Python ONNX Runtime)",
348
- available: true
349
- });
350
- return scores.toSorted((a, b) => b.score - a.score);
238
+ return null;
351
239
  }
352
240
  };
353
241
  //#endregion
@@ -6,25 +6,32 @@ export declare class PlatformScorer {
6
6
  * Path to the embedded portable Python (from `ctx.deps.ensurePython()`).
7
7
  * The Docker hub ships NO system `python3` — inference runs on the
8
8
  * downloaded portable build — so probing system `python3`/`python` would
9
- * wrongly report "Python not found" and never surface coreml/openvino
10
- * backends. When set, the Python module probes run against THIS binary;
11
- * `null` falls back to a system `python3`/`python` lookup (dev / agent
12
- * environments where Python is on PATH).
9
+ * wrongly report "Python not found". When set, the interpreter lookup
10
+ * tries THIS binary first; `null` falls back to a system `python3`/`python`
11
+ * lookup (dev / agent environments where Python is on PATH).
13
12
  */
14
13
  private readonly embeddedPythonPath;
15
14
  constructor(logger?: IScopedLogger, embeddedPythonPath?: string | null);
16
15
  /**
17
- * Probe hardware + runtimes and score all backend combos.
16
+ * Probe hardware + derive scores from pure rules and score all backend combos.
18
17
  *
19
18
  * An optional `onPhase` callback is invoked at each step so consumers
20
19
  * (e.g. the platform-probe addon) can emit live progress events on
21
20
  * the event bus. The callback takes a phase id + a typed payload; all
22
- * phases fire in strict order: `started` → `hardware` → `node-backends`
23
- * `python-backends` `scored` `done`. On failure, `error` fires
24
- * once with the exception. Cached after first call.
21
+ * phases fire in strict order: `started` → `hardware` → `scored` → `done`.
22
+ * On failure, `error` fires once with the exception. Cached after first call.
23
+ *
24
+ * Scores are hardware-driven only (no Python module import subprocess):
25
+ * `scoreRuntimes` from `@camstack/types` is the single source of truth.
25
26
  */
26
27
  probe(onPhase?: (phase: string, payload?: Record<string, unknown>) => void): Promise<PlatformCapabilities>;
27
28
  probeHardware(): Promise<HardwareInfo>;
28
- private probePythonBackends;
29
- private scoreBackends;
29
+ /**
30
+ * Locates a Python interpreter without importing any module.
31
+ *
32
+ * Tries: embedded path (if configured) → `python3` → `python`.
33
+ * Returns the first executable found, or `null` if none respond.
34
+ * MUST NOT run `-c "import ..."` — interpreter location only.
35
+ */
36
+ private resolvePythonPath;
30
37
  }
@@ -161,7 +161,7 @@ var StorageOrchestratorService = class {
161
161
  const resolver = this.nodeLocalResolver;
162
162
  if (!resolver) return;
163
163
  let stamped = 0;
164
- for (const [id, loc] of [...this.locations]) {
164
+ for (const [id, loc] of Array.from(this.locations)) {
165
165
  if (loc.nodeId) continue;
166
166
  if (resolver(loc.providerId) !== true) continue;
167
167
  const updated = {
@@ -301,7 +301,7 @@ var StorageOrchestratorService = class {
301
301
  this.logger.warn("storage-orchestrator: skipping prune — no location declarations loaded (fail-safe)");
302
302
  return;
303
303
  }
304
- for (const [id, loc] of [...this.locations]) {
304
+ for (const [id, loc] of Array.from(this.locations)) {
305
305
  if (this.registry.cardinalityOf(loc.type) !== null) continue;
306
306
  if (loc.isSystem) {
307
307
  this.locations.delete(id);
@@ -154,7 +154,7 @@ var StorageOrchestratorService = class {
154
154
  const resolver = this.nodeLocalResolver;
155
155
  if (!resolver) return;
156
156
  let stamped = 0;
157
- for (const [id, loc] of [...this.locations]) {
157
+ for (const [id, loc] of Array.from(this.locations)) {
158
158
  if (loc.nodeId) continue;
159
159
  if (resolver(loc.providerId) !== true) continue;
160
160
  const updated = {
@@ -294,7 +294,7 @@ var StorageOrchestratorService = class {
294
294
  this.logger.warn("storage-orchestrator: skipping prune — no location declarations loaded (fail-safe)");
295
295
  return;
296
296
  }
297
- for (const [id, loc] of [...this.locations]) {
297
+ for (const [id, loc] of Array.from(this.locations)) {
298
298
  if (this.registry.cardinalityOf(loc.type) !== null) continue;
299
299
  if (loc.isSystem) {
300
300
  this.locations.delete(id);
@@ -0,0 +1,38 @@
1
+ //#region src/kernel/custom-action-registry.ts
2
+ /**
3
+ * CustomActionRegistry — per-process registry of addon custom actions.
4
+ *
5
+ * Populated at boot from each addon's `AddonInitResult.customActions` +
6
+ * `handleCustomAction` handler. Rejects actions declared with scope other
7
+ * than 'system' (today only 'system' is runtime-supported; the descriptor
8
+ * allows future scopes for forward compat).
9
+ */
10
+ var CustomActionRegistry = class {
11
+ byAddon = /* @__PURE__ */ new Map();
12
+ registerAddon(addonId, catalog, handler) {
13
+ const actions = /* @__PURE__ */ new Map();
14
+ for (const [name, spec] of Object.entries(catalog)) {
15
+ const scope = spec.scope ?? { kind: "system" };
16
+ if (scope.kind !== "system") throw new Error(`custom action '${addonId}.${name}' declared scope '${scope.kind}' — not yet implemented`);
17
+ actions.set(name, {
18
+ spec,
19
+ handler: (input) => handler(name, input)
20
+ });
21
+ }
22
+ this.byAddon.set(addonId, actions);
23
+ }
24
+ unregisterAddon(addonId) {
25
+ this.byAddon.delete(addonId);
26
+ }
27
+ resolve(addonId, action) {
28
+ return this.byAddon.get(addonId)?.get(action) ?? null;
29
+ }
30
+ listActions(addonId) {
31
+ return [...this.byAddon.get(addonId)?.keys() ?? []];
32
+ }
33
+ listAddons() {
34
+ return [...this.byAddon.keys()];
35
+ }
36
+ };
37
+ //#endregion
38
+ export { CustomActionRegistry as t };
@@ -0,0 +1,43 @@
1
+ //#region src/kernel/custom-action-registry.ts
2
+ /**
3
+ * CustomActionRegistry — per-process registry of addon custom actions.
4
+ *
5
+ * Populated at boot from each addon's `AddonInitResult.customActions` +
6
+ * `handleCustomAction` handler. Rejects actions declared with scope other
7
+ * than 'system' (today only 'system' is runtime-supported; the descriptor
8
+ * allows future scopes for forward compat).
9
+ */
10
+ var CustomActionRegistry = class {
11
+ byAddon = /* @__PURE__ */ new Map();
12
+ registerAddon(addonId, catalog, handler) {
13
+ const actions = /* @__PURE__ */ new Map();
14
+ for (const [name, spec] of Object.entries(catalog)) {
15
+ const scope = spec.scope ?? { kind: "system" };
16
+ if (scope.kind !== "system") throw new Error(`custom action '${addonId}.${name}' declared scope '${scope.kind}' — not yet implemented`);
17
+ actions.set(name, {
18
+ spec,
19
+ handler: (input) => handler(name, input)
20
+ });
21
+ }
22
+ this.byAddon.set(addonId, actions);
23
+ }
24
+ unregisterAddon(addonId) {
25
+ this.byAddon.delete(addonId);
26
+ }
27
+ resolve(addonId, action) {
28
+ return this.byAddon.get(addonId)?.get(action) ?? null;
29
+ }
30
+ listActions(addonId) {
31
+ return [...this.byAddon.get(addonId)?.keys() ?? []];
32
+ }
33
+ listAddons() {
34
+ return [...this.byAddon.keys()];
35
+ }
36
+ };
37
+ //#endregion
38
+ Object.defineProperty(exports, "CustomActionRegistry", {
39
+ enumerable: true,
40
+ get: function() {
41
+ return CustomActionRegistry;
42
+ }
43
+ });