@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.
@@ -23,6 +23,14 @@ export interface CapCallOutMessage {
23
23
  readonly method: string;
24
24
  readonly args: unknown;
25
25
  readonly deviceId?: number;
26
+ /**
27
+ * Optional per-call node pin (out-of-band; NOT part of the validated method
28
+ * args). When set, the parent routes this singleton/in-process cap call to
29
+ * the named node's provider instead of the default (hub) resolution — used to
30
+ * query a remote agent's in-process singleton (e.g. the agent's own
31
+ * `platform-probe` hardware). Set via `ctx.api`'s `onNode(nodeId)` modifier.
32
+ */
33
+ readonly nodeId?: string;
26
34
  }
27
35
  /** Child → parent: a system event produced by the child to be forwarded to the hub event bus. */
28
36
  export interface ChildEventMessage {
@@ -59,6 +67,8 @@ export interface CapCallMessage {
59
67
  readonly method: string;
60
68
  readonly args: unknown;
61
69
  readonly deviceId?: number;
70
+ /** Optional per-call node pin (out-of-band routing hint; see {@link CapCallOutMessage.nodeId}). */
71
+ readonly nodeId?: string;
62
72
  }
63
73
  /** Parent → child: a system event delivered by the hub for the child to handle locally. */
64
74
  export interface ParentEventMessage {
@@ -5,6 +5,7 @@ let node_fs = require("node:fs");
5
5
  node_fs = require_chunk.__toESM(node_fs);
6
6
  let node_path = require("node:path");
7
7
  node_path = require_chunk.__toESM(node_path);
8
+ let _camstack_types = require("@camstack/types");
8
9
  let node_crypto = require("node:crypto");
9
10
  node_crypto = require_chunk.__toESM(node_crypto);
10
11
  let node_child_process = require("node:child_process");
@@ -3647,7 +3648,8 @@ var LocalChildRegistry = class {
3647
3648
  capName: out.capName,
3648
3649
  method: out.method,
3649
3650
  args: out.args,
3650
- ...out.deviceId !== void 0 ? { deviceId: out.deviceId } : {}
3651
+ ...out.deviceId !== void 0 ? { deviceId: out.deviceId } : {},
3652
+ ...out.nodeId !== void 0 ? { nodeId: out.nodeId } : {}
3651
3653
  };
3652
3654
  if (this.resolveChildId(out.capName, out.deviceId) !== null) {
3653
3655
  if (!this.egressRoutedCaps.has(out.capName)) {
@@ -3892,7 +3894,8 @@ var LocalChildClient = class {
3892
3894
  capName: input.capName,
3893
3895
  method: input.method,
3894
3896
  args: input.args,
3895
- ...input.deviceId !== void 0 ? { deviceId: input.deviceId } : {}
3897
+ ...input.deviceId !== void 0 ? { deviceId: input.deviceId } : {},
3898
+ ...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {}
3896
3899
  };
3897
3900
  return this.channel.request(msg);
3898
3901
  }
@@ -4586,7 +4589,10 @@ function createParentUnownedCallHandler(deps) {
4586
4589
  const deviceId = input.deviceId ?? extractDeviceId(input.args);
4587
4590
  const resolver = deps.getResolver();
4588
4591
  if (resolver !== null) try {
4589
- const route = resolver.resolveCapRoute(input.capName, deviceId !== void 0 ? { deviceId } : {});
4592
+ const route = resolver.resolveCapRoute(input.capName, {
4593
+ ...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {},
4594
+ ...deviceId !== void 0 ? { deviceId } : {}
4595
+ });
4590
4596
  return await resolver.dispatch(route, input.method, input.args);
4591
4597
  } catch (err) {
4592
4598
  if (!(err instanceof CapRouteError) || err.reason !== "no-provider") throw err;
@@ -4623,7 +4629,7 @@ function createParentUnownedCallHandler(deps) {
4623
4629
  return await brokerCallForCap(deps.broker, input.capName, input.method, input.args);
4624
4630
  } catch (cause) {
4625
4631
  const reason = cause instanceof Error ? cause.message : String(cause);
4626
- throw new Error(`parent could not route child unowned cap call "${input.capName}.${input.method}": ${reason}`, cause instanceof Error ? { cause } : void 0);
4632
+ throw new Error(`parent could not route child unowned cap call "${input.capName}.${input.method}": ${reason}`, { cause });
4627
4633
  }
4628
4634
  };
4629
4635
  }
@@ -5031,11 +5037,13 @@ function ipcParentLink(getCallOut) {
5031
5037
  const input = op.input;
5032
5038
  const inputObj = input && typeof input === "object" && !Array.isArray(input) ? input : null;
5033
5039
  const deviceId = inputObj && typeof Reflect.get(inputObj, "deviceId") === "number" ? Reflect.get(inputObj, "deviceId") : void 0;
5040
+ const pinnedNodeId = (0, _camstack_types.readNodePin)(op.context);
5034
5041
  const capCallInput = {
5035
5042
  capName: parsed.capName,
5036
5043
  method: parsed.method,
5037
5044
  args: op.input,
5038
- ...deviceId !== void 0 ? { deviceId } : {}
5045
+ ...deviceId !== void 0 ? { deviceId } : {},
5046
+ ...pinnedNodeId !== void 0 ? { nodeId: pinnedNodeId } : {}
5039
5047
  };
5040
5048
  return observable((observer) => {
5041
5049
  callOut(capCallInput).then((data) => {
@@ -5100,7 +5108,7 @@ var CapUsageRegistry = class {
5100
5108
  let window = byCap.get(rec.capName);
5101
5109
  if (!window) {
5102
5110
  window = {
5103
- buckets: new Array(this.retentionSeconds).fill(0),
5111
+ buckets: Array.from({ length: this.retentionSeconds }, () => 0),
5104
5112
  lastCallAtMs: 0
5105
5113
  };
5106
5114
  byCap.set(rec.capName, window);
@@ -6277,16 +6285,25 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
6277
6285
  };
6278
6286
  const writeBlob = async (coll, key, value, namespace) => {
6279
6287
  if (!settingsApi) return;
6280
- await settingsApi.set.mutate(namespace !== void 0 ? {
6281
- namespace,
6282
- collection: coll,
6283
- key,
6284
- value
6285
- } : {
6286
- collection: coll,
6287
- key,
6288
- value
6289
- });
6288
+ try {
6289
+ await settingsApi.set.mutate(namespace !== void 0 ? {
6290
+ namespace,
6291
+ collection: coll,
6292
+ key,
6293
+ value
6294
+ } : {
6295
+ collection: coll,
6296
+ key,
6297
+ value
6298
+ });
6299
+ } catch (err) {
6300
+ const isTrpcError = err instanceof _trpc_client.TRPCClientError;
6301
+ scopedLogger.warn(`settings write failed (${coll}/${key}) — ${isTrpcError ? "transport error" : "store error"}`, { meta: {
6302
+ collection: coll,
6303
+ key,
6304
+ error: (0, _camstack_types_addon.errMsg)(err)
6305
+ } });
6306
+ }
6290
6307
  };
6291
6308
  const rmwChains = /* @__PURE__ */ new Map();
6292
6309
  const serializeRmw = (coll, key, op) => {
@@ -6295,7 +6312,7 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
6295
6312
  rmwChains.set(chainKey, next);
6296
6313
  next.finally(() => {
6297
6314
  if (rmwChains.get(chainKey) === next) rmwChains.delete(chainKey);
6298
- });
6315
+ }).catch(() => void 0);
6299
6316
  return next;
6300
6317
  };
6301
6318
  const settingsView = {
@@ -4,6 +4,7 @@ import { createServer } from "node:http";
4
4
  import * as fs from "node:fs";
5
5
  import * as path$1 from "node:path";
6
6
  import { isAbsolute, join } from "node:path";
7
+ import { readNodePin } from "@camstack/types";
7
8
  import * as crypto$1 from "node:crypto";
8
9
  import { randomBytes, randomUUID } from "node:crypto";
9
10
  import { execFile } from "node:child_process";
@@ -11,7 +12,7 @@ import { promisify } from "node:util";
11
12
  import * as os from "node:os";
12
13
  import { tmpdir } from "node:os";
13
14
  import { unlink } from "node:fs/promises";
14
- import { DATAPLANE_SECRET_HEADER, DeviceType, DisposerChain, EventCategory, ReadinessRegistry, asJsonObject, asString, createDeviceProxy, deviceOpsCapability, emitReadiness, errMsg, expandCapMethods, scopeKey, sleep } from "@camstack/types/addon";
15
+ import { DATAPLANE_SECRET_HEADER as DATAPLANE_SECRET_HEADER$1, DeviceType as DeviceType$1, DisposerChain, EventCategory as EventCategory$1, ReadinessRegistry, asJsonObject as asJsonObject$1, asString as asString$1, createDeviceProxy, deviceOpsCapability, emitReadiness, errMsg as errMsg$1, expandCapMethods, scopeKey, sleep as sleep$1 } from "@camstack/types/addon";
15
16
  import { TRPCClientError, createTRPCClient } from "@trpc/client";
16
17
  import { connect, createServer as createServer$1 } from "node:net";
17
18
  //#region src/kernel/addon-class-resolver.ts
@@ -102,7 +103,7 @@ async function installManifestNativeDeps(addonDir, pkgRaw, logger, registry) {
102
103
  } catch (err) {
103
104
  logger.warn("Failed to write native deps marker", { meta: {
104
105
  markerFile,
105
- error: errMsg(err)
106
+ error: errMsg$1(err)
106
107
  } });
107
108
  }
108
109
  return;
@@ -129,7 +130,7 @@ async function installManifestNativeDeps(addonDir, pkgRaw, logger, registry) {
129
130
  timeout: 3e5
130
131
  });
131
132
  } catch (err) {
132
- throw new Error(`npm install of native deps failed for ${addonDir}: ${errMsg(err)}`, { cause: err });
133
+ throw new Error(`npm install of native deps failed for ${addonDir}: ${errMsg$1(err)}`, { cause: err });
133
134
  }
134
135
  await rebuildNativeDeps(addonDir, pending.map(([name]) => name), logger);
135
136
  try {
@@ -137,7 +138,7 @@ async function installManifestNativeDeps(addonDir, pkgRaw, logger, registry) {
137
138
  } catch (err) {
138
139
  logger.warn("Failed to write native deps marker", { meta: {
139
140
  markerFile,
140
- error: errMsg(err)
141
+ error: errMsg$1(err)
141
142
  } });
142
143
  }
143
144
  }
@@ -174,7 +175,7 @@ function copyPrebuiltNativeDep(name, addonDir, logger) {
174
175
  logger.warn("Prebuilt native dep copy failed — will try npm install", { meta: {
175
176
  name,
176
177
  source,
177
- error: errMsg(err)
178
+ error: errMsg$1(err)
178
179
  } });
179
180
  }
180
181
  }
@@ -207,13 +208,13 @@ function candidateHoistedDirs(name, addonDir) {
207
208
  }
208
209
  /** Read & validate `camstack.nativeDependencies`. Returns null when absent. */
209
210
  function readNativeDeps(pkgRaw) {
210
- const camstack = asJsonObject(pkgRaw["camstack"]);
211
+ const camstack = asJsonObject$1(pkgRaw["camstack"]);
211
212
  if (!camstack) return null;
212
- const native = asJsonObject(camstack["nativeDependencies"]);
213
+ const native = asJsonObject$1(camstack["nativeDependencies"]);
213
214
  if (!native) return null;
214
215
  const out = {};
215
216
  for (const [k, v] of Object.entries(native)) {
216
- const range = asString(v);
217
+ const range = asString$1(v);
217
218
  if (range) out[k] = range;
218
219
  }
219
220
  return Object.keys(out).length > 0 ? out : null;
@@ -280,7 +281,7 @@ async function rebuildNativeDeps(addonDir, packageNames, logger) {
280
281
  } catch (err) {
281
282
  logger.warn("Electron rebuild failed (continuing — prebuilt binary may be present)", { meta: {
282
283
  addonDir,
283
- error: errMsg(err)
284
+ error: errMsg$1(err)
284
285
  } });
285
286
  }
286
287
  return;
@@ -298,7 +299,7 @@ async function rebuildNativeDeps(addonDir, packageNames, logger) {
298
299
  } catch (err) {
299
300
  logger.warn("npm rebuild failed (continuing — prebuilt binary may be present)", { meta: {
300
301
  addonDir,
301
- error: errMsg(err)
302
+ error: errMsg$1(err)
302
303
  } });
303
304
  }
304
305
  }
@@ -1031,7 +1032,7 @@ async function callWithServiceDiscoveryRetry(broker, actionName, input) {
1031
1032
  if (!(err instanceof Error ? err.message : String(err)).includes("is not found")) throw err;
1032
1033
  attempt++;
1033
1034
  if (attempt === 1) broker.logger.warn(`native-cap-bridge: service not found for "${actionName}", retrying with exponential backoff…`);
1034
- await sleep(delay);
1035
+ await sleep$1(delay);
1035
1036
  delay = Math.min(delay * 2, BACKOFF_MAX_MS);
1036
1037
  }
1037
1038
  }
@@ -1136,7 +1137,7 @@ function createBrokerDeviceManagerApi(opts) {
1136
1137
  parentDeviceId,
1137
1138
  logger: (() => {
1138
1139
  const base = opts.logger.child(stableId);
1139
- const containerDeviceId = parentDeviceId ?? (deviceMeta?.type === DeviceType.Container ? id : null);
1140
+ const containerDeviceId = parentDeviceId ?? (deviceMeta?.type === DeviceType$1.Container ? id : null);
1140
1141
  return base.withTags(containerDeviceId !== null ? {
1141
1142
  deviceId: id,
1142
1143
  containerDeviceId
@@ -1162,7 +1163,7 @@ function createBrokerDeviceManagerApi(opts) {
1162
1163
  id,
1163
1164
  stableId,
1164
1165
  addonId,
1165
- type: DeviceType.Camera,
1166
+ type: DeviceType$1.Camera,
1166
1167
  name: stableId,
1167
1168
  location: null,
1168
1169
  disabled: false,
@@ -1228,7 +1229,7 @@ function createBrokerDeviceManagerApi(opts) {
1228
1229
  type: "device",
1229
1230
  id
1230
1231
  },
1231
- category: EventCategory.DeviceBindingsChanged,
1232
+ category: EventCategory$1.DeviceBindingsChanged,
1232
1233
  data: {
1233
1234
  deviceId: id,
1234
1235
  capName: cap.name,
@@ -1612,7 +1613,7 @@ function createBrokerDeviceManagerApi(opts) {
1612
1613
  type: "device",
1613
1614
  id: device.id
1614
1615
  },
1615
- category: EventCategory.DeviceReady,
1616
+ category: EventCategory$1.DeviceReady,
1616
1617
  data: { deviceId: device.id }
1617
1618
  });
1618
1619
  };
@@ -1751,7 +1752,7 @@ function createBrokerDeviceManagerApi(opts) {
1751
1752
  type: "device",
1752
1753
  id: deviceId
1753
1754
  },
1754
- category: EventCategory.DeviceBindingsChanged,
1755
+ category: EventCategory$1.DeviceBindingsChanged,
1755
1756
  data: {
1756
1757
  deviceId,
1757
1758
  capName,
@@ -3646,7 +3647,8 @@ var LocalChildRegistry = class {
3646
3647
  capName: out.capName,
3647
3648
  method: out.method,
3648
3649
  args: out.args,
3649
- ...out.deviceId !== void 0 ? { deviceId: out.deviceId } : {}
3650
+ ...out.deviceId !== void 0 ? { deviceId: out.deviceId } : {},
3651
+ ...out.nodeId !== void 0 ? { nodeId: out.nodeId } : {}
3650
3652
  };
3651
3653
  if (this.resolveChildId(out.capName, out.deviceId) !== null) {
3652
3654
  if (!this.egressRoutedCaps.has(out.capName)) {
@@ -3891,7 +3893,8 @@ var LocalChildClient = class {
3891
3893
  capName: input.capName,
3892
3894
  method: input.method,
3893
3895
  args: input.args,
3894
- ...input.deviceId !== void 0 ? { deviceId: input.deviceId } : {}
3896
+ ...input.deviceId !== void 0 ? { deviceId: input.deviceId } : {},
3897
+ ...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {}
3895
3898
  };
3896
3899
  return this.channel.request(msg);
3897
3900
  }
@@ -4585,7 +4588,10 @@ function createParentUnownedCallHandler(deps) {
4585
4588
  const deviceId = input.deviceId ?? extractDeviceId(input.args);
4586
4589
  const resolver = deps.getResolver();
4587
4590
  if (resolver !== null) try {
4588
- const route = resolver.resolveCapRoute(input.capName, deviceId !== void 0 ? { deviceId } : {});
4591
+ const route = resolver.resolveCapRoute(input.capName, {
4592
+ ...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {},
4593
+ ...deviceId !== void 0 ? { deviceId } : {}
4594
+ });
4589
4595
  return await resolver.dispatch(route, input.method, input.args);
4590
4596
  } catch (err) {
4591
4597
  if (!(err instanceof CapRouteError) || err.reason !== "no-provider") throw err;
@@ -4622,7 +4628,7 @@ function createParentUnownedCallHandler(deps) {
4622
4628
  return await brokerCallForCap(deps.broker, input.capName, input.method, input.args);
4623
4629
  } catch (cause) {
4624
4630
  const reason = cause instanceof Error ? cause.message : String(cause);
4625
- throw new Error(`parent could not route child unowned cap call "${input.capName}.${input.method}": ${reason}`, cause instanceof Error ? { cause } : void 0);
4631
+ throw new Error(`parent could not route child unowned cap call "${input.capName}.${input.method}": ${reason}`, { cause });
4626
4632
  }
4627
4633
  };
4628
4634
  }
@@ -5030,11 +5036,13 @@ function ipcParentLink(getCallOut) {
5030
5036
  const input = op.input;
5031
5037
  const inputObj = input && typeof input === "object" && !Array.isArray(input) ? input : null;
5032
5038
  const deviceId = inputObj && typeof Reflect.get(inputObj, "deviceId") === "number" ? Reflect.get(inputObj, "deviceId") : void 0;
5039
+ const pinnedNodeId = readNodePin(op.context);
5033
5040
  const capCallInput = {
5034
5041
  capName: parsed.capName,
5035
5042
  method: parsed.method,
5036
5043
  args: op.input,
5037
- ...deviceId !== void 0 ? { deviceId } : {}
5044
+ ...deviceId !== void 0 ? { deviceId } : {},
5045
+ ...pinnedNodeId !== void 0 ? { nodeId: pinnedNodeId } : {}
5038
5046
  };
5039
5047
  return observable((observer) => {
5040
5048
  callOut(capCallInput).then((data) => {
@@ -5099,7 +5107,7 @@ var CapUsageRegistry = class {
5099
5107
  let window = byCap.get(rec.capName);
5100
5108
  if (!window) {
5101
5109
  window = {
5102
- buckets: new Array(this.retentionSeconds).fill(0),
5110
+ buckets: Array.from({ length: this.retentionSeconds }, () => 0),
5103
5111
  lastCallAtMs: 0
5104
5112
  };
5105
5113
  byCap.set(rec.capName, window);
@@ -5319,7 +5327,7 @@ function createAddonDataPlaneFacility(args) {
5319
5327
  let server = null;
5320
5328
  let baseUrl = "";
5321
5329
  const route = (req, res) => {
5322
- if (req.headers[DATAPLANE_SECRET_HEADER] !== secret) {
5330
+ if (req.headers[DATAPLANE_SECRET_HEADER$1] !== secret) {
5323
5331
  res.writeHead(403).end();
5324
5332
  return;
5325
5333
  }
@@ -5438,7 +5446,7 @@ function resolveNodeRoot() {
5438
5446
  var inFlight = /* @__PURE__ */ new Map();
5439
5447
  var LOCK_STALE_MS = 10 * 6e4;
5440
5448
  var LOCK_POLL_MS = 500;
5441
- function sleep$1(ms) {
5449
+ function sleep$2(ms) {
5442
5450
  return new Promise((resolve) => setTimeout(resolve, ms));
5443
5451
  }
5444
5452
  /**
@@ -5457,7 +5465,7 @@ async function withFileLock(lockPath, fn) {
5457
5465
  } catch {
5458
5466
  try {
5459
5467
  if (Date.now() - fs.statSync(lockPath).mtimeMs > LOCK_STALE_MS) fs.rmSync(lockPath, { force: true });
5460
- else await sleep$1(LOCK_POLL_MS);
5468
+ else await sleep$2(LOCK_POLL_MS);
5461
5469
  } catch {}
5462
5470
  }
5463
5471
  try {
@@ -6249,7 +6257,7 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
6249
6257
  } });
6250
6258
  registerWorkerDisposerChain(nodeId, addonId, workerDisposerChain);
6251
6259
  const bindingCache = /* @__PURE__ */ new Map();
6252
- scopedEventBus.subscribe({ category: EventCategory.DeviceBindingsChanged }, (e) => {
6260
+ scopedEventBus.subscribe({ category: EventCategory$1.DeviceBindingsChanged }, (e) => {
6253
6261
  const data = e.data ?? {};
6254
6262
  if (typeof data.deviceId === "number") {
6255
6263
  bindingCache.delete(data.deviceId);
@@ -6276,16 +6284,25 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
6276
6284
  };
6277
6285
  const writeBlob = async (coll, key, value, namespace) => {
6278
6286
  if (!settingsApi) return;
6279
- await settingsApi.set.mutate(namespace !== void 0 ? {
6280
- namespace,
6281
- collection: coll,
6282
- key,
6283
- value
6284
- } : {
6285
- collection: coll,
6286
- key,
6287
- value
6288
- });
6287
+ try {
6288
+ await settingsApi.set.mutate(namespace !== void 0 ? {
6289
+ namespace,
6290
+ collection: coll,
6291
+ key,
6292
+ value
6293
+ } : {
6294
+ collection: coll,
6295
+ key,
6296
+ value
6297
+ });
6298
+ } catch (err) {
6299
+ const isTrpcError = err instanceof TRPCClientError;
6300
+ scopedLogger.warn(`settings write failed (${coll}/${key}) — ${isTrpcError ? "transport error" : "store error"}`, { meta: {
6301
+ collection: coll,
6302
+ key,
6303
+ error: errMsg$1(err)
6304
+ } });
6305
+ }
6289
6306
  };
6290
6307
  const rmwChains = /* @__PURE__ */ new Map();
6291
6308
  const serializeRmw = (coll, key, op) => {
@@ -6294,7 +6311,7 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
6294
6311
  rmwChains.set(chainKey, next);
6295
6312
  next.finally(() => {
6296
6313
  if (rmwChains.get(chainKey) === next) rmwChains.delete(chainKey);
6297
- });
6314
+ }).catch(() => void 0);
6298
6315
  return next;
6299
6316
  };
6300
6317
  const settingsView = {
@@ -301,6 +301,24 @@ function createFileDataPlaneHandler(opts) {
301
301
  }
302
302
  //#endregion
303
303
  //#region src/download/model-downloader.ts
304
+ function isNonEmptyFile(filePath) {
305
+ return node_fs.existsSync(filePath) && node_fs.statSync(filePath).size > 0;
306
+ }
307
+ /**
308
+ * Sibling files of a single-file (non-directory) format — extra files the
309
+ * format needs, fetched from the same remote directory as `url` and stored
310
+ * flat alongside the main file in `modelsDir`. Catalog-declared via
311
+ * `formatEntry.files`, e.g. OpenVINO IR lists its `.bin` weights next to the
312
+ * `.xml`. The downloader stays format-agnostic; the convention lives in the
313
+ * catalog data (like `MLPACKAGE_FILES` for the directory case).
314
+ */
315
+ function siblingFilesFor(formatEntry) {
316
+ return formatEntry.isDirectory ? [] : formatEntry.files ?? [];
317
+ }
318
+ /** Resolve a sibling's remote URL relative to the main file's directory. */
319
+ function siblingUrl(mainUrl, sibling) {
320
+ return mainUrl.replace(/[^/]+$/, sibling);
321
+ }
304
322
  /** Build fetch headers, including HF auth token for huggingface.co URLs */
305
323
  function buildHeaders(url) {
306
324
  const headers = { "User-Agent": "CamStack/1.0" };
@@ -466,14 +484,18 @@ async function ensureModel(modelsDir, entry, format, onProgress) {
466
484
  if (entry.extraFiles) for (const extra of entry.extraFiles) await downloadFile(extra.url, node_path.join(modelsDir, extra.filename));
467
485
  const filename = formatEntry.url.split("/").pop() ?? `${entry.id}.${format}`;
468
486
  const modelPath = node_path.join(modelsDir, filename);
487
+ const siblings = siblingFilesFor(formatEntry);
469
488
  if (node_fs.existsSync(modelPath)) if (formatEntry.isDirectory && !node_fs.existsSync(node_path.join(modelPath, "Manifest.json"))) node_fs.rmSync(modelPath, {
470
489
  recursive: true,
471
490
  force: true
472
491
  });
473
- else return modelPath;
492
+ else if (siblings.some((f) => !isNonEmptyFile(node_path.join(modelsDir, f)))) {} else return modelPath;
474
493
  node_fs.mkdirSync(modelsDir, { recursive: true });
475
494
  if (formatEntry.isDirectory) await downloadDirectory(formatEntry.url, modelPath, formatEntry.files, onProgress);
476
- else await downloadFile(formatEntry.url, modelPath, (downloaded, total) => onProgress?.(downloaded, total === 0 ? void 0 : total));
495
+ else {
496
+ await downloadFile(formatEntry.url, modelPath, (downloaded, total) => onProgress?.(downloaded, total === 0 ? void 0 : total));
497
+ for (const sibling of siblings) await downloadFile(siblingUrl(formatEntry.url, sibling), node_path.join(modelsDir, sibling));
498
+ }
477
499
  return modelPath;
478
500
  }
479
501
  /** Compute the on-disk path for a given model + format, even when not yet downloaded. */
@@ -490,17 +512,25 @@ function isModelDownloaded(modelsDir, entry, format) {
490
512
  const modelPath = getModelFilePath(modelsDir, entry, format);
491
513
  if (!modelPath || !node_fs.existsSync(modelPath)) return false;
492
514
  if (formatEntry.isDirectory) return node_fs.existsSync(node_path.join(modelPath, "Manifest.json"));
493
- return node_fs.statSync(modelPath).size > 0;
515
+ if (node_fs.statSync(modelPath).size <= 0) return false;
516
+ return siblingFilesFor(formatEntry).every((f) => isNonEmptyFile(node_path.join(modelsDir, f)));
494
517
  }
495
518
  /** Remove the on-disk model file/directory. Returns true if something was deleted. */
496
519
  function deleteModelFromDisk(modelsDir, entry, format) {
497
520
  const modelPath = getModelFilePath(modelsDir, entry, format);
498
521
  if (!modelPath || !node_fs.existsSync(modelPath)) return false;
499
- if (entry.formats[format]?.isDirectory) node_fs.rmSync(modelPath, {
522
+ const formatEntry = entry.formats[format];
523
+ if (formatEntry?.isDirectory) node_fs.rmSync(modelPath, {
500
524
  recursive: true,
501
525
  force: true
502
526
  });
503
- else node_fs.unlinkSync(modelPath);
527
+ else {
528
+ node_fs.unlinkSync(modelPath);
529
+ if (formatEntry) for (const sibling of siblingFilesFor(formatEntry)) {
530
+ const sibPath = node_path.join(modelsDir, sibling);
531
+ if (node_fs.existsSync(sibPath)) node_fs.unlinkSync(sibPath);
532
+ }
533
+ }
504
534
  return true;
505
535
  }
506
536
  //#endregion
@@ -300,6 +300,24 @@ function createFileDataPlaneHandler(opts) {
300
300
  }
301
301
  //#endregion
302
302
  //#region src/download/model-downloader.ts
303
+ function isNonEmptyFile(filePath) {
304
+ return fs.existsSync(filePath) && fs.statSync(filePath).size > 0;
305
+ }
306
+ /**
307
+ * Sibling files of a single-file (non-directory) format — extra files the
308
+ * format needs, fetched from the same remote directory as `url` and stored
309
+ * flat alongside the main file in `modelsDir`. Catalog-declared via
310
+ * `formatEntry.files`, e.g. OpenVINO IR lists its `.bin` weights next to the
311
+ * `.xml`. The downloader stays format-agnostic; the convention lives in the
312
+ * catalog data (like `MLPACKAGE_FILES` for the directory case).
313
+ */
314
+ function siblingFilesFor(formatEntry) {
315
+ return formatEntry.isDirectory ? [] : formatEntry.files ?? [];
316
+ }
317
+ /** Resolve a sibling's remote URL relative to the main file's directory. */
318
+ function siblingUrl(mainUrl, sibling) {
319
+ return mainUrl.replace(/[^/]+$/, sibling);
320
+ }
303
321
  /** Build fetch headers, including HF auth token for huggingface.co URLs */
304
322
  function buildHeaders(url) {
305
323
  const headers = { "User-Agent": "CamStack/1.0" };
@@ -465,14 +483,18 @@ async function ensureModel(modelsDir, entry, format, onProgress) {
465
483
  if (entry.extraFiles) for (const extra of entry.extraFiles) await downloadFile(extra.url, path$1.join(modelsDir, extra.filename));
466
484
  const filename = formatEntry.url.split("/").pop() ?? `${entry.id}.${format}`;
467
485
  const modelPath = path$1.join(modelsDir, filename);
486
+ const siblings = siblingFilesFor(formatEntry);
468
487
  if (fs.existsSync(modelPath)) if (formatEntry.isDirectory && !fs.existsSync(path$1.join(modelPath, "Manifest.json"))) fs.rmSync(modelPath, {
469
488
  recursive: true,
470
489
  force: true
471
490
  });
472
- else return modelPath;
491
+ else if (siblings.some((f) => !isNonEmptyFile(path$1.join(modelsDir, f)))) {} else return modelPath;
473
492
  fs.mkdirSync(modelsDir, { recursive: true });
474
493
  if (formatEntry.isDirectory) await downloadDirectory(formatEntry.url, modelPath, formatEntry.files, onProgress);
475
- else await downloadFile(formatEntry.url, modelPath, (downloaded, total) => onProgress?.(downloaded, total === 0 ? void 0 : total));
494
+ else {
495
+ await downloadFile(formatEntry.url, modelPath, (downloaded, total) => onProgress?.(downloaded, total === 0 ? void 0 : total));
496
+ for (const sibling of siblings) await downloadFile(siblingUrl(formatEntry.url, sibling), path$1.join(modelsDir, sibling));
497
+ }
476
498
  return modelPath;
477
499
  }
478
500
  /** Compute the on-disk path for a given model + format, even when not yet downloaded. */
@@ -489,17 +511,25 @@ function isModelDownloaded(modelsDir, entry, format) {
489
511
  const modelPath = getModelFilePath(modelsDir, entry, format);
490
512
  if (!modelPath || !fs.existsSync(modelPath)) return false;
491
513
  if (formatEntry.isDirectory) return fs.existsSync(path$1.join(modelPath, "Manifest.json"));
492
- return fs.statSync(modelPath).size > 0;
514
+ if (fs.statSync(modelPath).size <= 0) return false;
515
+ return siblingFilesFor(formatEntry).every((f) => isNonEmptyFile(path$1.join(modelsDir, f)));
493
516
  }
494
517
  /** Remove the on-disk model file/directory. Returns true if something was deleted. */
495
518
  function deleteModelFromDisk(modelsDir, entry, format) {
496
519
  const modelPath = getModelFilePath(modelsDir, entry, format);
497
520
  if (!modelPath || !fs.existsSync(modelPath)) return false;
498
- if (entry.formats[format]?.isDirectory) fs.rmSync(modelPath, {
521
+ const formatEntry = entry.formats[format];
522
+ if (formatEntry?.isDirectory) fs.rmSync(modelPath, {
499
523
  recursive: true,
500
524
  force: true
501
525
  });
502
- else fs.unlinkSync(modelPath);
526
+ else {
527
+ fs.unlinkSync(modelPath);
528
+ if (formatEntry) for (const sibling of siblingFilesFor(formatEntry)) {
529
+ const sibPath = path$1.join(modelsDir, sibling);
530
+ if (fs.existsSync(sibPath)) fs.unlinkSync(sibPath);
531
+ }
532
+ }
503
533
  return true;
504
534
  }
505
535
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/system",
3
- "version": "1.0.7",
3
+ "version": "1.1.0",
4
4
  "description": "Core addon for CamStack — builtins, pipeline, process management, auth, logging, events",
5
5
  "keywords": [
6
6
  "camstack",