@camstack/system 1.0.7 → 1.1.0

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.
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("./chunk-Cek0wNdY.js");
2
- const require_manifest_python_deps = require("./manifest-python-deps-eBDj5HEY.js");
2
+ const require_manifest_python_deps = require("./manifest-python-deps-BWURo7dc.js");
3
3
  const require_custom_action_registry = require("./custom-action-registry-vLYEFTtv.js");
4
4
  let node_fs = require("node:fs");
5
5
  node_fs = require_chunk.__toESM(node_fs);
@@ -170,10 +170,10 @@ function toSerializableRoute(route) {
170
170
  */
171
171
  async function dispatchSettings(addon, addonId, method, args) {
172
172
  switch (method) {
173
- case "getGlobalSettings": return typeof addon.getGlobalSettings === "function" ? await addon.getGlobalSettings(args.overlay, args.cap) ?? null : null;
173
+ case "getGlobalSettings": return typeof addon.getGlobalSettings === "function" ? await addon.getGlobalSettings(args.overlay, args.cap, args.nodeId) ?? null : null;
174
174
  case "updateGlobalSettings":
175
175
  if (typeof addon.updateGlobalSettings !== "function") throw new Error(`child-addon-call: addon "${addonId}" does not implement updateGlobalSettings`);
176
- await addon.updateGlobalSettings(args.patch ?? {});
176
+ await addon.updateGlobalSettings(args.patch ?? {}, args.nodeId);
177
177
  return { success: true };
178
178
  case "getDeviceSettings": return typeof addon.getDeviceSettings === "function" && typeof args.deviceId === "number" ? await addon.getDeviceSettings(args.deviceId) ?? null : null;
179
179
  case "updateDeviceSettings":
@@ -1,4 +1,4 @@
1
- import { $ as setWorkerNativeCapsChangeListener, A as createUdsLoggerWithControl, L as LocalChildClient, X as getWorkerNativeCapProvider, Z as getWorkerNativeCapSnapshot, i as createUdsAddonContext, mt as resolveAddonClass, pt as installManifestNativeDeps, t as installManifestPythonDeps, tt as validateProviderRegistrations } from "./manifest-python-deps-CoJXeb9u.mjs";
1
+ import { $ as setWorkerNativeCapsChangeListener, A as createUdsLoggerWithControl, L as LocalChildClient, X as getWorkerNativeCapProvider, Z as getWorkerNativeCapSnapshot, i as createUdsAddonContext, mt as resolveAddonClass, pt as installManifestNativeDeps, t as installManifestPythonDeps, tt as validateProviderRegistrations } from "./manifest-python-deps-BcrTzHH_.mjs";
2
2
  import { t as CustomActionRegistry } from "./custom-action-registry-BEXwC-oo.mjs";
3
3
  import { register } from "node:module";
4
4
  import * as fs from "node:fs";
@@ -166,10 +166,10 @@ function toSerializableRoute(route) {
166
166
  */
167
167
  async function dispatchSettings(addon, addonId, method, args) {
168
168
  switch (method) {
169
- case "getGlobalSettings": return typeof addon.getGlobalSettings === "function" ? await addon.getGlobalSettings(args.overlay, args.cap) ?? null : null;
169
+ case "getGlobalSettings": return typeof addon.getGlobalSettings === "function" ? await addon.getGlobalSettings(args.overlay, args.cap, args.nodeId) ?? null : null;
170
170
  case "updateGlobalSettings":
171
171
  if (typeof addon.updateGlobalSettings !== "function") throw new Error(`child-addon-call: addon "${addonId}" does not implement updateGlobalSettings`);
172
- await addon.updateGlobalSettings(args.patch ?? {});
172
+ await addon.updateGlobalSettings(args.patch ?? {}, args.nodeId);
173
173
  return { success: true };
174
174
  case "getDeviceSettings": return typeof addon.getDeviceSettings === "function" && typeof args.deviceId === "number" ? await addon.getDeviceSettings(args.deviceId) ?? null : null;
175
175
  case "updateDeviceSettings":
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  require("./chunk-Cek0wNdY.js");
3
- const require_model_download_service = require("./model-download-service-JtVQtbb6.js");
3
+ const require_model_download_service = require("./model-download-service-1eEOkNeS.js");
4
4
  const require_custom_action_registry = require("./custom-action-registry-vLYEFTtv.js");
5
5
  exports.CustomActionRegistry = require_custom_action_registry.CustomActionRegistry;
6
6
  exports.ModelDownloadService = require_model_download_service.ModelDownloadService;
@@ -1,3 +1,3 @@
1
- import { a as ensureModel, c as isModelDownloaded, l as createFileDataPlaneHandler, n as deleteModelFromDisk, r as downloadFile, t as ModelDownloadService } from "./model-download-service-C7AjBsX9.mjs";
1
+ import { a as ensureModel, c as isModelDownloaded, l as createFileDataPlaneHandler, n as deleteModelFromDisk, r as downloadFile, t as ModelDownloadService } from "./model-download-service-RxAOiYvX.mjs";
2
2
  import { t as CustomActionRegistry } from "./custom-action-registry-BEXwC-oo.mjs";
3
3
  export { CustomActionRegistry, ModelDownloadService, createFileDataPlaneHandler, deleteModelFromDisk, downloadFile, ensureModel, isModelDownloaded };
@@ -1638,8 +1638,8 @@ var DeviceManagerAddon = class DeviceManagerAddon extends _camstack_types.BaseAd
1638
1638
  if (!persisted) return null;
1639
1639
  const links = (persisted.meta.deviceLinks ?? []).filter((l) => l.target.cap === cap);
1640
1640
  if (links.length === 0) return null;
1641
- const registry = this.capabilityRegistry;
1642
- const schema = registry?.getDefinition(cap)?.status?.schema;
1641
+ const capRegistry = this.capabilityRegistry;
1642
+ const schema = capRegistry?.getDefinition(cap)?.status?.schema;
1643
1643
  if (!schema) return null;
1644
1644
  const allMeta = await readMeta();
1645
1645
  let containerStableId = persisted.stableId;
@@ -1655,7 +1655,7 @@ var DeviceManagerAddon = class DeviceManagerAddon extends _camstack_types.BaseAd
1655
1655
  const srcId = resolveSourceDeviceId(containerStableId, link.source.sourceKey, allMeta);
1656
1656
  if (srcId === null) continue;
1657
1657
  try {
1658
- const srcProvider = registry?.getProviderForDevice(link.source.cap, srcId);
1658
+ const srcProvider = capRegistry?.getProviderForDevice(link.source.cap, srcId);
1659
1659
  const raw = (0, _camstack_types.getByPath)(typeof srcProvider?.getStatus === "function" ? await srcProvider.getStatus({ deviceId: srcId }) : void 0, link.source.fieldPath);
1660
1660
  resolved.push({
1661
1661
  link,
@@ -1840,7 +1840,7 @@ var DeviceManagerAddon = class DeviceManagerAddon extends _camstack_types.BaseAd
1840
1840
  this.capabilityRegistry?.unregisterAllNativeForDevice(deviceId);
1841
1841
  idToAddonId.delete(deviceId);
1842
1842
  this.devicesWithLinks.delete(deviceId);
1843
- for (const key of this.lastEmittedOverlay.keys()) if (key.startsWith(`${deviceId}:`)) this.lastEmittedOverlay.delete(key);
1843
+ for (const overlayKey of this.lastEmittedOverlay.keys()) if (overlayKey.startsWith(`${deviceId}:`)) this.lastEmittedOverlay.delete(overlayKey);
1844
1844
  await rebuildLinkDependents();
1845
1845
  this.ctx.logger.info("removed device", { tags: {
1846
1846
  deviceId,
@@ -2386,7 +2386,7 @@ var DeviceManagerAddon = class DeviceManagerAddon extends _camstack_types.BaseAd
2386
2386
  listLocations: async () => {
2387
2387
  const store = await settings.readAddonStore();
2388
2388
  const meta = store.deviceMeta ?? {};
2389
- const registry = store.locations ?? [];
2389
+ const locations = store.locations ?? [];
2390
2390
  const seen = /* @__PURE__ */ new Map();
2391
2391
  const consider = (raw) => {
2392
2392
  if (typeof raw !== "string") return;
@@ -2395,7 +2395,7 @@ var DeviceManagerAddon = class DeviceManagerAddon extends _camstack_types.BaseAd
2395
2395
  const key = trimmed.toLowerCase();
2396
2396
  if (!seen.has(key)) seen.set(key, trimmed);
2397
2397
  };
2398
- for (const label of registry) consider(label);
2398
+ for (const label of locations) consider(label);
2399
2399
  for (const m of Object.values(meta)) consider(m.location);
2400
2400
  return [...seen.values()].toSorted((a, b) => a.localeCompare(b, void 0, { sensitivity: "base" }));
2401
2401
  },
@@ -2960,9 +2960,9 @@ var DeviceManagerAddon = class DeviceManagerAddon extends _camstack_types.BaseAd
2960
2960
  return this.getDeviceAggregate(input.deviceId, "live");
2961
2961
  },
2962
2962
  getDeviceAggregate: async (input) => {
2963
- const [settings, live] = await Promise.all([this.getDeviceAggregate(input.deviceId, "settings"), this.getDeviceAggregate(input.deviceId, "live")]);
2963
+ const [settingsAggregate, live] = await Promise.all([this.getDeviceAggregate(input.deviceId, "settings"), this.getDeviceAggregate(input.deviceId, "live")]);
2964
2964
  return {
2965
- settings,
2965
+ settings: settingsAggregate,
2966
2966
  live
2967
2967
  };
2968
2968
  },
@@ -1633,8 +1633,8 @@ var DeviceManagerAddon = class DeviceManagerAddon extends BaseAddon {
1633
1633
  if (!persisted) return null;
1634
1634
  const links = (persisted.meta.deviceLinks ?? []).filter((l) => l.target.cap === cap);
1635
1635
  if (links.length === 0) return null;
1636
- const registry = this.capabilityRegistry;
1637
- const schema = registry?.getDefinition(cap)?.status?.schema;
1636
+ const capRegistry = this.capabilityRegistry;
1637
+ const schema = capRegistry?.getDefinition(cap)?.status?.schema;
1638
1638
  if (!schema) return null;
1639
1639
  const allMeta = await readMeta();
1640
1640
  let containerStableId = persisted.stableId;
@@ -1650,7 +1650,7 @@ var DeviceManagerAddon = class DeviceManagerAddon extends BaseAddon {
1650
1650
  const srcId = resolveSourceDeviceId(containerStableId, link.source.sourceKey, allMeta);
1651
1651
  if (srcId === null) continue;
1652
1652
  try {
1653
- const srcProvider = registry?.getProviderForDevice(link.source.cap, srcId);
1653
+ const srcProvider = capRegistry?.getProviderForDevice(link.source.cap, srcId);
1654
1654
  const raw = getByPath(typeof srcProvider?.getStatus === "function" ? await srcProvider.getStatus({ deviceId: srcId }) : void 0, link.source.fieldPath);
1655
1655
  resolved.push({
1656
1656
  link,
@@ -1835,7 +1835,7 @@ var DeviceManagerAddon = class DeviceManagerAddon extends BaseAddon {
1835
1835
  this.capabilityRegistry?.unregisterAllNativeForDevice(deviceId);
1836
1836
  idToAddonId.delete(deviceId);
1837
1837
  this.devicesWithLinks.delete(deviceId);
1838
- for (const key of this.lastEmittedOverlay.keys()) if (key.startsWith(`${deviceId}:`)) this.lastEmittedOverlay.delete(key);
1838
+ for (const overlayKey of this.lastEmittedOverlay.keys()) if (overlayKey.startsWith(`${deviceId}:`)) this.lastEmittedOverlay.delete(overlayKey);
1839
1839
  await rebuildLinkDependents();
1840
1840
  this.ctx.logger.info("removed device", { tags: {
1841
1841
  deviceId,
@@ -2381,7 +2381,7 @@ var DeviceManagerAddon = class DeviceManagerAddon extends BaseAddon {
2381
2381
  listLocations: async () => {
2382
2382
  const store = await settings.readAddonStore();
2383
2383
  const meta = store.deviceMeta ?? {};
2384
- const registry = store.locations ?? [];
2384
+ const locations = store.locations ?? [];
2385
2385
  const seen = /* @__PURE__ */ new Map();
2386
2386
  const consider = (raw) => {
2387
2387
  if (typeof raw !== "string") return;
@@ -2390,7 +2390,7 @@ var DeviceManagerAddon = class DeviceManagerAddon extends BaseAddon {
2390
2390
  const key = trimmed.toLowerCase();
2391
2391
  if (!seen.has(key)) seen.set(key, trimmed);
2392
2392
  };
2393
- for (const label of registry) consider(label);
2393
+ for (const label of locations) consider(label);
2394
2394
  for (const m of Object.values(meta)) consider(m.location);
2395
2395
  return [...seen.values()].toSorted((a, b) => a.localeCompare(b, void 0, { sensitivity: "base" }));
2396
2396
  },
@@ -2955,9 +2955,9 @@ var DeviceManagerAddon = class DeviceManagerAddon extends BaseAddon {
2955
2955
  return this.getDeviceAggregate(input.deviceId, "live");
2956
2956
  },
2957
2957
  getDeviceAggregate: async (input) => {
2958
- const [settings, live] = await Promise.all([this.getDeviceAggregate(input.deviceId, "settings"), this.getDeviceAggregate(input.deviceId, "live")]);
2958
+ const [settingsAggregate, live] = await Promise.all([this.getDeviceAggregate(input.deviceId, "settings"), this.getDeviceAggregate(input.deviceId, "live")]);
2959
2959
  return {
2960
- settings,
2960
+ settings: settingsAggregate,
2961
2961
  live
2962
2962
  };
2963
2963
  },
@@ -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
  }