@camstack/system 1.1.38 → 1.1.39

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-B6ahe90k.js");
2
+ const require_manifest_python_deps = require("./manifest-python-deps-BGvIVCm_.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);
@@ -1,4 +1,4 @@
1
- import { F as createUdsLoggerWithControl, I as LocalChildClient, i as createUdsAddonContext, it as setWorkerNativeCapsChangeListener, nt as getWorkerNativeCapSnapshot, ot as validateProviderRegistrations, t as installManifestPythonDeps, tt as getWorkerNativeCapProvider, vt as installManifestNativeDeps, yt as resolveAddonClass } from "./manifest-python-deps-4QvOLiBn.mjs";
1
+ import { F as createUdsLoggerWithControl, I as LocalChildClient, i as createUdsAddonContext, it as setWorkerNativeCapsChangeListener, nt as getWorkerNativeCapSnapshot, ot as validateProviderRegistrations, t as installManifestPythonDeps, tt as getWorkerNativeCapProvider, vt as installManifestNativeDeps, yt as resolveAddonClass } from "./manifest-python-deps-BjxBfiFd.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";
@@ -3021,6 +3021,7 @@ var DEFAULT_BACKUP_LOCATIONS = [
3021
3021
  ];
3022
3022
  /** Path of the manifest file embedded inside every archive. */
3023
3023
  var ARCHIVE_MANIFEST_NAME = ".camstack-backup-manifest.json";
3024
+ var DEFAULT_STAT_CACHE_TTL_MS = 6e4;
3024
3025
  /**
3025
3026
  * System-level archive primitives. Construct one per server boot;
3026
3027
  * methods are pure I/O.
@@ -3028,33 +3029,55 @@ var ARCHIVE_MANIFEST_NAME = ".camstack-backup-manifest.json";
3028
3029
  var SystemBackupService = class {
3029
3030
  dataDir;
3030
3031
  logger;
3031
- constructor(dataDir, logger) {
3032
+ statCacheTtlMs;
3033
+ statCache = /* @__PURE__ */ new Map();
3034
+ constructor(dataDir, logger, opts) {
3032
3035
  this.dataDir = dataDir;
3033
3036
  this.logger = logger;
3037
+ this.statCacheTtlMs = opts?.statCacheTtlMs ?? DEFAULT_STAT_CACHE_TTL_MS;
3034
3038
  }
3035
3039
  /**
3036
3040
  * Snapshot the current size + file count of each well-known
3037
3041
  * location. Used by the admin UI to render an opt-in checklist
3038
3042
  * before triggering a backup, and by `createArchive` to derive the
3039
3043
  * embedded manifest.
3044
+ *
3045
+ * The walk is async (yields to the event loop per directory) and
3046
+ * per-location results are cached + single-flighted: the service
3047
+ * runs in-process in the hub, and a synchronous walk of the 70k+
3048
+ * file `addons` location blocked the event loop long enough to kill
3049
+ * WS keepalives cluster-wide.
3040
3050
  */
3041
3051
  statLocations(locations = DEFAULT_BACKUP_LOCATIONS) {
3042
- return locations.map((loc) => {
3043
- const abs = node_path.isAbsolute(loc) ? loc : node_path.join(this.dataDir, loc);
3044
- if (!node_fs.existsSync(abs)) return {
3045
- name: loc,
3046
- sizeBytes: 0,
3047
- fileCount: 0,
3048
- present: false
3049
- };
3050
- const walked = walkAbs(abs, abs);
3051
- return {
3052
- name: loc,
3053
- sizeBytes: walked.reduce((acc, e) => acc + e.sizeBytes, 0),
3054
- fileCount: walked.filter((e) => e.kind === "file").length,
3055
- present: true
3056
- };
3052
+ return Promise.all(locations.map((loc) => this.statLocationCached(loc)));
3053
+ }
3054
+ statLocationCached(loc) {
3055
+ const now = Date.now();
3056
+ const hit = this.statCache.get(loc);
3057
+ if (hit && now - hit.at < this.statCacheTtlMs) return hit.value;
3058
+ const value = this.statLocation(loc);
3059
+ this.statCache.set(loc, {
3060
+ at: now,
3061
+ value
3057
3062
  });
3063
+ value.catch(() => this.statCache.delete(loc));
3064
+ return value;
3065
+ }
3066
+ async statLocation(loc) {
3067
+ const abs = node_path.isAbsolute(loc) ? loc : node_path.join(this.dataDir, loc);
3068
+ if (!node_fs.existsSync(abs)) return {
3069
+ name: loc,
3070
+ sizeBytes: 0,
3071
+ fileCount: 0,
3072
+ present: false
3073
+ };
3074
+ const walked = await walkAbs(abs, abs);
3075
+ return {
3076
+ name: loc,
3077
+ sizeBytes: walked.reduce((acc, e) => acc + e.sizeBytes, 0),
3078
+ fileCount: walked.filter((e) => e.kind === "file").length,
3079
+ present: true
3080
+ };
3058
3081
  }
3059
3082
  /**
3060
3083
  * Create a tar.gz of the given locations. Uses the `tar` npm package
@@ -3082,8 +3105,8 @@ var SystemBackupService = class {
3082
3105
  const entries = [];
3083
3106
  for (const loc of present) {
3084
3107
  const abs = node_path.join(this.dataDir, loc);
3085
- const stat = node_fs.statSync(abs);
3086
- if (stat.isDirectory()) for (const e of walkAbs(abs, this.dataDir)) entries.push(e);
3108
+ const stat = await node_fs_promises.stat(abs);
3109
+ if (stat.isDirectory()) for (const e of await walkAbs(abs, this.dataDir)) entries.push(e);
3087
3110
  else entries.push(toArchiveEntry(abs, this.dataDir, stat));
3088
3111
  }
3089
3112
  const manifest = {
@@ -3258,15 +3281,15 @@ var SystemBackupService = class {
3258
3281
  * path. Symlinks are dereferenced (matches the `follow: true` flag we
3259
3282
  * pass to tar.create).
3260
3283
  */
3261
- function walkAbs(absRoot, dataDir) {
3284
+ async function walkAbs(absRoot, dataDir) {
3262
3285
  const out = [];
3263
- walkRec(absRoot, dataDir, out);
3286
+ await walkRec(absRoot, dataDir, out);
3264
3287
  return out;
3265
3288
  }
3266
- function walkRec(abs, dataDir, out) {
3289
+ async function walkRec(abs, dataDir, out) {
3267
3290
  let stat;
3268
3291
  try {
3269
- stat = node_fs.statSync(abs);
3292
+ stat = await node_fs_promises.stat(abs);
3270
3293
  } catch {
3271
3294
  return;
3272
3295
  }
@@ -3274,11 +3297,11 @@ function walkRec(abs, dataDir, out) {
3274
3297
  if (stat.isDirectory()) {
3275
3298
  let children;
3276
3299
  try {
3277
- children = node_fs.readdirSync(abs);
3300
+ children = await node_fs_promises.readdir(abs);
3278
3301
  } catch {
3279
3302
  return;
3280
3303
  }
3281
- for (const child of children) walkRec(node_path.join(abs, child), dataDir, out);
3304
+ for (const child of children) await walkRec(node_path.join(abs, child), dataDir, out);
3282
3305
  }
3283
3306
  }
3284
3307
  function toArchiveEntry(abs, dataDir, stat) {
@@ -3006,6 +3006,7 @@ var DEFAULT_BACKUP_LOCATIONS = [
3006
3006
  ];
3007
3007
  /** Path of the manifest file embedded inside every archive. */
3008
3008
  var ARCHIVE_MANIFEST_NAME = ".camstack-backup-manifest.json";
3009
+ var DEFAULT_STAT_CACHE_TTL_MS = 6e4;
3009
3010
  /**
3010
3011
  * System-level archive primitives. Construct one per server boot;
3011
3012
  * methods are pure I/O.
@@ -3013,33 +3014,55 @@ var ARCHIVE_MANIFEST_NAME = ".camstack-backup-manifest.json";
3013
3014
  var SystemBackupService = class {
3014
3015
  dataDir;
3015
3016
  logger;
3016
- constructor(dataDir, logger) {
3017
+ statCacheTtlMs;
3018
+ statCache = /* @__PURE__ */ new Map();
3019
+ constructor(dataDir, logger, opts) {
3017
3020
  this.dataDir = dataDir;
3018
3021
  this.logger = logger;
3022
+ this.statCacheTtlMs = opts?.statCacheTtlMs ?? DEFAULT_STAT_CACHE_TTL_MS;
3019
3023
  }
3020
3024
  /**
3021
3025
  * Snapshot the current size + file count of each well-known
3022
3026
  * location. Used by the admin UI to render an opt-in checklist
3023
3027
  * before triggering a backup, and by `createArchive` to derive the
3024
3028
  * embedded manifest.
3029
+ *
3030
+ * The walk is async (yields to the event loop per directory) and
3031
+ * per-location results are cached + single-flighted: the service
3032
+ * runs in-process in the hub, and a synchronous walk of the 70k+
3033
+ * file `addons` location blocked the event loop long enough to kill
3034
+ * WS keepalives cluster-wide.
3025
3035
  */
3026
3036
  statLocations(locations = DEFAULT_BACKUP_LOCATIONS) {
3027
- return locations.map((loc) => {
3028
- const abs = path$1.isAbsolute(loc) ? loc : path$1.join(this.dataDir, loc);
3029
- if (!fs.existsSync(abs)) return {
3030
- name: loc,
3031
- sizeBytes: 0,
3032
- fileCount: 0,
3033
- present: false
3034
- };
3035
- const walked = walkAbs(abs, abs);
3036
- return {
3037
- name: loc,
3038
- sizeBytes: walked.reduce((acc, e) => acc + e.sizeBytes, 0),
3039
- fileCount: walked.filter((e) => e.kind === "file").length,
3040
- present: true
3041
- };
3037
+ return Promise.all(locations.map((loc) => this.statLocationCached(loc)));
3038
+ }
3039
+ statLocationCached(loc) {
3040
+ const now = Date.now();
3041
+ const hit = this.statCache.get(loc);
3042
+ if (hit && now - hit.at < this.statCacheTtlMs) return hit.value;
3043
+ const value = this.statLocation(loc);
3044
+ this.statCache.set(loc, {
3045
+ at: now,
3046
+ value
3042
3047
  });
3048
+ value.catch(() => this.statCache.delete(loc));
3049
+ return value;
3050
+ }
3051
+ async statLocation(loc) {
3052
+ const abs = path$1.isAbsolute(loc) ? loc : path$1.join(this.dataDir, loc);
3053
+ if (!fs.existsSync(abs)) return {
3054
+ name: loc,
3055
+ sizeBytes: 0,
3056
+ fileCount: 0,
3057
+ present: false
3058
+ };
3059
+ const walked = await walkAbs(abs, abs);
3060
+ return {
3061
+ name: loc,
3062
+ sizeBytes: walked.reduce((acc, e) => acc + e.sizeBytes, 0),
3063
+ fileCount: walked.filter((e) => e.kind === "file").length,
3064
+ present: true
3065
+ };
3043
3066
  }
3044
3067
  /**
3045
3068
  * Create a tar.gz of the given locations. Uses the `tar` npm package
@@ -3067,8 +3090,8 @@ var SystemBackupService = class {
3067
3090
  const entries = [];
3068
3091
  for (const loc of present) {
3069
3092
  const abs = path$1.join(this.dataDir, loc);
3070
- const stat = fs.statSync(abs);
3071
- if (stat.isDirectory()) for (const e of walkAbs(abs, this.dataDir)) entries.push(e);
3093
+ const stat = await fsp.stat(abs);
3094
+ if (stat.isDirectory()) for (const e of await walkAbs(abs, this.dataDir)) entries.push(e);
3072
3095
  else entries.push(toArchiveEntry(abs, this.dataDir, stat));
3073
3096
  }
3074
3097
  const manifest = {
@@ -3243,15 +3266,15 @@ var SystemBackupService = class {
3243
3266
  * path. Symlinks are dereferenced (matches the `follow: true` flag we
3244
3267
  * pass to tar.create).
3245
3268
  */
3246
- function walkAbs(absRoot, dataDir) {
3269
+ async function walkAbs(absRoot, dataDir) {
3247
3270
  const out = [];
3248
- walkRec(absRoot, dataDir, out);
3271
+ await walkRec(absRoot, dataDir, out);
3249
3272
  return out;
3250
3273
  }
3251
- function walkRec(abs, dataDir, out) {
3274
+ async function walkRec(abs, dataDir, out) {
3252
3275
  let stat;
3253
3276
  try {
3254
- stat = fs.statSync(abs);
3277
+ stat = await fsp.stat(abs);
3255
3278
  } catch {
3256
3279
  return;
3257
3280
  }
@@ -3259,11 +3282,11 @@ function walkRec(abs, dataDir, out) {
3259
3282
  if (stat.isDirectory()) {
3260
3283
  let children;
3261
3284
  try {
3262
- children = fs.readdirSync(abs);
3285
+ children = await fsp.readdir(abs);
3263
3286
  } catch {
3264
3287
  return;
3265
3288
  }
3266
- for (const child of children) walkRec(path$1.join(abs, child), dataDir, out);
3289
+ for (const child of children) await walkRec(path$1.join(abs, child), dataDir, out);
3267
3290
  }
3268
3291
  }
3269
3292
  function toArchiveEntry(abs, dataDir, stat) {
@@ -64,6 +64,14 @@ export interface LocationStat {
64
64
  /** False when the location doesn't exist under dataDir. */
65
65
  readonly present: boolean;
66
66
  }
67
+ export interface SystemBackupServiceOptions {
68
+ /**
69
+ * How long a `statLocations` walk result stays fresh. The `addons`
70
+ * location holds tens of thousands of files; without a cache every
71
+ * admin-UI visit re-walks it.
72
+ */
73
+ readonly statCacheTtlMs?: number;
74
+ }
67
75
  /**
68
76
  * System-level archive primitives. Construct one per server boot;
69
77
  * methods are pure I/O.
@@ -71,14 +79,24 @@ export interface LocationStat {
71
79
  export declare class SystemBackupService {
72
80
  private readonly dataDir;
73
81
  private readonly logger;
74
- constructor(dataDir: string, logger: IScopedLogger);
82
+ private readonly statCacheTtlMs;
83
+ private readonly statCache;
84
+ constructor(dataDir: string, logger: IScopedLogger, opts?: SystemBackupServiceOptions);
75
85
  /**
76
86
  * Snapshot the current size + file count of each well-known
77
87
  * location. Used by the admin UI to render an opt-in checklist
78
88
  * before triggering a backup, and by `createArchive` to derive the
79
89
  * embedded manifest.
90
+ *
91
+ * The walk is async (yields to the event loop per directory) and
92
+ * per-location results are cached + single-flighted: the service
93
+ * runs in-process in the hub, and a synchronous walk of the 70k+
94
+ * file `addons` location blocked the event loop long enough to kill
95
+ * WS keepalives cluster-wide.
80
96
  */
81
- statLocations(locations?: readonly string[]): readonly LocationStat[];
97
+ statLocations(locations?: readonly string[]): Promise<readonly LocationStat[]>;
98
+ private statLocationCached;
99
+ private statLocation;
82
100
  /**
83
101
  * Create a tar.gz of the given locations. Uses the `tar` npm package
84
102
  * (used by npm itself) instead of shelling out — gives us:
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@ const require_builtins_local_auth_local_auth_addon = require("./builtins/local-a
21
21
  require("./builtins/local-auth/index.js");
22
22
  const require_builtins_device_manager_device_manager_addon = require("./builtins/device-manager/device-manager.addon.js");
23
23
  require("./builtins/device-manager/index.js");
24
- const require_manifest_python_deps = require("./manifest-python-deps-B6ahe90k.js");
24
+ const require_manifest_python_deps = require("./manifest-python-deps-BGvIVCm_.js");
25
25
  const require_custom_action_registry = require("./custom-action-registry-vLYEFTtv.js");
26
26
  let _camstack_types_node = require("@camstack/types/node");
27
27
  let zod = require("zod");
@@ -2587,6 +2587,71 @@ async function copyDirRecursive(src, dest) {
2587
2587
  });
2588
2588
  }
2589
2589
  /**
2590
+ * Runtime natives that must stay installable even when an addon forgets to
2591
+ * declare them: the mirror of the build preset's `COMMON_NATIVE_DEPS`
2592
+ * (tools/build/vite-lib.preset.ts — build tooling, not loadable at runtime).
2593
+ * These are the ONLY packages a self-contained dist genuinely `require()`s
2594
+ * from node_modules; everything else is already inlined by the bundle.
2595
+ */
2596
+ var RUNTIME_NATIVE_KEEP = [
2597
+ "better-sqlite3",
2598
+ "node-av",
2599
+ "onnxruntime-node",
2600
+ "sharp",
2601
+ "ssh2",
2602
+ "werift",
2603
+ "systeminformation"
2604
+ ];
2605
+ /** Read `camstack.runtimeDependencies` (extra non-native deps the dist
2606
+ * requires at runtime — versions come from the manifest's own dep maps). */
2607
+ function readRuntimeDependencies(pkg) {
2608
+ const camstack = pkg.camstack;
2609
+ if (camstack === null || typeof camstack !== "object" || Array.isArray(camstack)) return [];
2610
+ const raw = camstack.runtimeDependencies;
2611
+ if (!Array.isArray(raw)) return [];
2612
+ return raw.filter((entry) => typeof entry === "string");
2613
+ }
2614
+ /**
2615
+ * Strip every dependency an addon's self-contained dist has already bundled,
2616
+ * keeping ONLY what must resolve from node_modules at runtime:
2617
+ *
2618
+ * keep = RUNTIME_NATIVE_KEEP ∪ camstack.runtimeDependencies (minus @camstack/*)
2619
+ *
2620
+ * Addon dists are fully bundled (vite `externals: 'self-contained'`), yet the
2621
+ * blanket `npm install` of the manifest's dependencies re-installed the whole
2622
+ * tree the dist already contains — 98% of the deployed addons dir was
2623
+ * redundant node_modules (74k files / 1.34 GB on the live hub; it stalled the
2624
+ * backup walk and bloated every deploy). Kept PEER deps are folded into
2625
+ * `dependencies` so `--omit=peer` installs can't drop them (pipeline's
2626
+ * patched `werift`).
2627
+ *
2628
+ * Escape hatch: `CAMSTACK_INSTALL_FULL_DEPS=1` restores the legacy behaviour
2629
+ * (strip only @camstack/*) for debugging a suspected undeclared runtime dep.
2630
+ *
2631
+ * Returns a new object (immutable).
2632
+ */
2633
+ function stripBundledDeps(pkg) {
2634
+ if (process.env.CAMSTACK_INSTALL_FULL_DEPS === "1") return stripCamstackDeps(pkg);
2635
+ const camstack = pkg.camstack;
2636
+ if (camstack !== null && typeof camstack === "object" && !Array.isArray(camstack) && camstack.system === true) return stripCamstackDeps(pkg);
2637
+ const keep = new Set([...RUNTIME_NATIVE_KEEP, ...readRuntimeDependencies(pkg)]);
2638
+ const kept = {};
2639
+ for (const depType of ["dependencies", "peerDependencies"]) {
2640
+ const rawDeps = pkg[depType];
2641
+ if (rawDeps === null || typeof rawDeps !== "object" || Array.isArray(rawDeps)) continue;
2642
+ for (const [name, version] of Object.entries(rawDeps)) {
2643
+ if (name.startsWith("@camstack/")) continue;
2644
+ if (!keep.has(name) || typeof version !== "string") continue;
2645
+ if (!(name in kept)) kept[name] = version;
2646
+ }
2647
+ }
2648
+ const result = { ...pkg };
2649
+ result.dependencies = Object.keys(kept).length > 0 ? kept : void 0;
2650
+ result.peerDependencies = void 0;
2651
+ delete result.devDependencies;
2652
+ return result;
2653
+ }
2654
+ /**
2590
2655
  * Strip @camstack/* dependencies and devDependencies from a package.json object.
2591
2656
  * Used when installing addons into the addons directory — @camstack packages
2592
2657
  * are provided by the host runtime, not installed per-addon.
@@ -3171,7 +3236,7 @@ var AddonInstaller = class AddonInstaller {
3171
3236
  force: true
3172
3237
  });
3173
3238
  ensureDir(targetDir);
3174
- await node_fs.promises.writeFile(node_path.join(targetDir, "package.json"), JSON.stringify(stripCamstackDeps(pkgData), null, 2));
3239
+ await node_fs.promises.writeFile(node_path.join(targetDir, "package.json"), JSON.stringify(stripBundledDeps(pkgData), null, 2));
3175
3240
  await copyDirRecursive(distDir, node_path.join(targetDir, "dist"));
3176
3241
  await copyExtraFileDirs(pkgData, sourceDir, targetDir);
3177
3242
  await node_fs.promises.writeFile(node_path.join(targetDir, ".install-source"), "local");
@@ -3180,7 +3245,7 @@ var AddonInstaller = class AddonInstaller {
3180
3245
  version: localPkgVersion,
3181
3246
  source: "local"
3182
3247
  });
3183
- const strippedDeps = stripCamstackDeps(pkgData);
3248
+ const strippedDeps = stripBundledDeps(pkgData);
3184
3249
  if (strippedDeps["dependencies"] && typeof strippedDeps["dependencies"] === "object" && Object.keys(strippedDeps["dependencies"]).length > 0) try {
3185
3250
  await execFileAsync("npm", [
3186
3251
  "install",
@@ -3358,7 +3423,7 @@ var AddonInstaller = class AddonInstaller {
3358
3423
  if (!pkgView) throw new Error(`Invalid package.json at ${pkgJsonPath}`);
3359
3424
  if (!pkgView.camstackAddons) throw new Error(`Package ${pkgView.name} has no camstack.addons manifest`);
3360
3425
  const targetDir = node_path.join(this.addonsDir, pkgView.name);
3361
- const strippedManifest = stripCamstackDeps(pkgView.raw);
3426
+ const strippedManifest = stripBundledDeps(pkgView.raw);
3362
3427
  await node_fs.promises.writeFile(pkgJsonPath, JSON.stringify(strippedManifest, null, 2));
3363
3428
  const strippedRuntimeDeps = strippedManifest["dependencies"];
3364
3429
  if (strippedRuntimeDeps != null && typeof strippedRuntimeDeps === "object" && Object.keys(strippedRuntimeDeps).length > 0) {
package/dist/index.mjs CHANGED
@@ -19,7 +19,7 @@ import { LocalAuthAddon, a as require_ms, c as __esmMin, d as __toCommonJS, f as
19
19
  import "./builtins/local-auth/index.mjs";
20
20
  import { DeviceManagerAddon } from "./builtins/device-manager/device-manager.addon.mjs";
21
21
  import "./builtins/device-manager/index.mjs";
22
- import { $ as buildUdsNativeCapProxy, A as createParentUnownedCallHandler, B as AGENT_CAP_FWD_SERVICE, C as buildLinkChain, D as HUB_CAP_FWD_ACTION, E as localProviderLink, F as createUdsLoggerWithControl, G as createLocalTransport, H as CapRouteError, I as LocalChildClient, J as SocketChannel, K as UdsLocalTransportClient, L as LocalChildRegistry, M as createUdsEventBus, N as udsChildLogToWorkerEntry, O as HUB_CAP_FWD_SERVICE, P as createUdsLogger, Q as buildNativeCapProxy, R as UDS_NO_ROUTE_PREFIX, S as brokerTransportLink, T as ipcParentLink, U as classifyCapRoute, V as CapRouteResolver, W as callWithServiceDiscovery, X as FrameDecoder, Y as localEndpointPath, Z as encodeFrame, _ as resolveHwAccel, _t as CapabilityUnavailableError, a as getWorkerDeviceRegistry, at as createAddonService, b as getCapUsageRegistry, c as setHubConnected, ct as capActionName, d as getMoleculerEventStats, dt as capServiceName, et as createBrokerDeviceManagerApi, f as registerEventBusService, ft as parseCapAction, g as createKernelHwAccel, gt as CapabilityHandle, h as AddonDepsManager, ht as DeviceRegistry, i as createUdsAddonContext, j as createUdsEventBridge, k as createHubCapForwardService, l as EVENT_TOPIC_PREFIX, lt as capActionSuffix, m as subscribePassthrough, mt as serializeTypedArrays, n as adaptBrokerToCluster, o as getOrInitReadinessRegistry, ot as validateProviderRegistrations, p as setNodeEventInterest, pt as deserializeTypedArrays, q as UdsLocalTransportServer, r as createAddonContext, rt as mountNativeCapService, s as getOrInitReadinessRegistryForClient, st as NATIVE_PROVIDER_SERVICE_INFIX, t as installManifestPythonDeps, u as getBrokerEventBus, ut as capBareAction, v as CapUsageRegistry, vt as installManifestNativeDeps, w as ipcChildLink, x as brokerCallForCap, y as __resetCapUsageRegistryForTests, yt as resolveAddonClass, z as AGENT_CAP_FWD_ACTION } from "./manifest-python-deps-4QvOLiBn.mjs";
22
+ import { $ as buildUdsNativeCapProxy, A as createParentUnownedCallHandler, B as AGENT_CAP_FWD_SERVICE, C as buildLinkChain, D as HUB_CAP_FWD_ACTION, E as localProviderLink, F as createUdsLoggerWithControl, G as createLocalTransport, H as CapRouteError, I as LocalChildClient, J as SocketChannel, K as UdsLocalTransportClient, L as LocalChildRegistry, M as createUdsEventBus, N as udsChildLogToWorkerEntry, O as HUB_CAP_FWD_SERVICE, P as createUdsLogger, Q as buildNativeCapProxy, R as UDS_NO_ROUTE_PREFIX, S as brokerTransportLink, T as ipcParentLink, U as classifyCapRoute, V as CapRouteResolver, W as callWithServiceDiscovery, X as FrameDecoder, Y as localEndpointPath, Z as encodeFrame, _ as resolveHwAccel, _t as CapabilityUnavailableError, a as getWorkerDeviceRegistry, at as createAddonService, b as getCapUsageRegistry, c as setHubConnected, ct as capActionName, d as getMoleculerEventStats, dt as capServiceName, et as createBrokerDeviceManagerApi, f as registerEventBusService, ft as parseCapAction, g as createKernelHwAccel, gt as CapabilityHandle, h as AddonDepsManager, ht as DeviceRegistry, i as createUdsAddonContext, j as createUdsEventBridge, k as createHubCapForwardService, l as EVENT_TOPIC_PREFIX, lt as capActionSuffix, m as subscribePassthrough, mt as serializeTypedArrays, n as adaptBrokerToCluster, o as getOrInitReadinessRegistry, ot as validateProviderRegistrations, p as setNodeEventInterest, pt as deserializeTypedArrays, q as UdsLocalTransportServer, r as createAddonContext, rt as mountNativeCapService, s as getOrInitReadinessRegistryForClient, st as NATIVE_PROVIDER_SERVICE_INFIX, t as installManifestPythonDeps, u as getBrokerEventBus, ut as capBareAction, v as CapUsageRegistry, vt as installManifestNativeDeps, w as ipcChildLink, x as brokerCallForCap, y as __resetCapUsageRegistryForTests, yt as resolveAddonClass, z as AGENT_CAP_FWD_ACTION } from "./manifest-python-deps-BjxBfiFd.mjs";
23
23
  import { t as CustomActionRegistry } from "./custom-action-registry-BEXwC-oo.mjs";
24
24
  import { PYTHON_VERSION, buildBinaryPath, downloadBinary, ensureBinary, ensureFfmpeg, ensurePython, findInPath, getFfmpegDownloadUrl, getPlatformInfo, getPythonDownloadUrl, installPythonPackages, installPythonRequirements } from "@camstack/types/node";
25
25
  import { z } from "zod";
@@ -2579,6 +2579,71 @@ async function copyDirRecursive(src, dest) {
2579
2579
  });
2580
2580
  }
2581
2581
  /**
2582
+ * Runtime natives that must stay installable even when an addon forgets to
2583
+ * declare them: the mirror of the build preset's `COMMON_NATIVE_DEPS`
2584
+ * (tools/build/vite-lib.preset.ts — build tooling, not loadable at runtime).
2585
+ * These are the ONLY packages a self-contained dist genuinely `require()`s
2586
+ * from node_modules; everything else is already inlined by the bundle.
2587
+ */
2588
+ var RUNTIME_NATIVE_KEEP = [
2589
+ "better-sqlite3",
2590
+ "node-av",
2591
+ "onnxruntime-node",
2592
+ "sharp",
2593
+ "ssh2",
2594
+ "werift",
2595
+ "systeminformation"
2596
+ ];
2597
+ /** Read `camstack.runtimeDependencies` (extra non-native deps the dist
2598
+ * requires at runtime — versions come from the manifest's own dep maps). */
2599
+ function readRuntimeDependencies(pkg) {
2600
+ const camstack = pkg.camstack;
2601
+ if (camstack === null || typeof camstack !== "object" || Array.isArray(camstack)) return [];
2602
+ const raw = camstack.runtimeDependencies;
2603
+ if (!Array.isArray(raw)) return [];
2604
+ return raw.filter((entry) => typeof entry === "string");
2605
+ }
2606
+ /**
2607
+ * Strip every dependency an addon's self-contained dist has already bundled,
2608
+ * keeping ONLY what must resolve from node_modules at runtime:
2609
+ *
2610
+ * keep = RUNTIME_NATIVE_KEEP ∪ camstack.runtimeDependencies (minus @camstack/*)
2611
+ *
2612
+ * Addon dists are fully bundled (vite `externals: 'self-contained'`), yet the
2613
+ * blanket `npm install` of the manifest's dependencies re-installed the whole
2614
+ * tree the dist already contains — 98% of the deployed addons dir was
2615
+ * redundant node_modules (74k files / 1.34 GB on the live hub; it stalled the
2616
+ * backup walk and bloated every deploy). Kept PEER deps are folded into
2617
+ * `dependencies` so `--omit=peer` installs can't drop them (pipeline's
2618
+ * patched `werift`).
2619
+ *
2620
+ * Escape hatch: `CAMSTACK_INSTALL_FULL_DEPS=1` restores the legacy behaviour
2621
+ * (strip only @camstack/*) for debugging a suspected undeclared runtime dep.
2622
+ *
2623
+ * Returns a new object (immutable).
2624
+ */
2625
+ function stripBundledDeps(pkg) {
2626
+ if (process.env.CAMSTACK_INSTALL_FULL_DEPS === "1") return stripCamstackDeps(pkg);
2627
+ const camstack = pkg.camstack;
2628
+ if (camstack !== null && typeof camstack === "object" && !Array.isArray(camstack) && camstack.system === true) return stripCamstackDeps(pkg);
2629
+ const keep = new Set([...RUNTIME_NATIVE_KEEP, ...readRuntimeDependencies(pkg)]);
2630
+ const kept = {};
2631
+ for (const depType of ["dependencies", "peerDependencies"]) {
2632
+ const rawDeps = pkg[depType];
2633
+ if (rawDeps === null || typeof rawDeps !== "object" || Array.isArray(rawDeps)) continue;
2634
+ for (const [name, version] of Object.entries(rawDeps)) {
2635
+ if (name.startsWith("@camstack/")) continue;
2636
+ if (!keep.has(name) || typeof version !== "string") continue;
2637
+ if (!(name in kept)) kept[name] = version;
2638
+ }
2639
+ }
2640
+ const result = { ...pkg };
2641
+ result.dependencies = Object.keys(kept).length > 0 ? kept : void 0;
2642
+ result.peerDependencies = void 0;
2643
+ delete result.devDependencies;
2644
+ return result;
2645
+ }
2646
+ /**
2582
2647
  * Strip @camstack/* dependencies and devDependencies from a package.json object.
2583
2648
  * Used when installing addons into the addons directory — @camstack packages
2584
2649
  * are provided by the host runtime, not installed per-addon.
@@ -3163,7 +3228,7 @@ var AddonInstaller = class AddonInstaller {
3163
3228
  force: true
3164
3229
  });
3165
3230
  ensureDir(targetDir);
3166
- await fs$17.promises.writeFile(path$39.join(targetDir, "package.json"), JSON.stringify(stripCamstackDeps(pkgData), null, 2));
3231
+ await fs$17.promises.writeFile(path$39.join(targetDir, "package.json"), JSON.stringify(stripBundledDeps(pkgData), null, 2));
3167
3232
  await copyDirRecursive(distDir, path$39.join(targetDir, "dist"));
3168
3233
  await copyExtraFileDirs(pkgData, sourceDir, targetDir);
3169
3234
  await fs$17.promises.writeFile(path$39.join(targetDir, ".install-source"), "local");
@@ -3172,7 +3237,7 @@ var AddonInstaller = class AddonInstaller {
3172
3237
  version: localPkgVersion,
3173
3238
  source: "local"
3174
3239
  });
3175
- const strippedDeps = stripCamstackDeps(pkgData);
3240
+ const strippedDeps = stripBundledDeps(pkgData);
3176
3241
  if (strippedDeps["dependencies"] && typeof strippedDeps["dependencies"] === "object" && Object.keys(strippedDeps["dependencies"]).length > 0) try {
3177
3242
  await execFileAsync("npm", [
3178
3243
  "install",
@@ -3350,7 +3415,7 @@ var AddonInstaller = class AddonInstaller {
3350
3415
  if (!pkgView) throw new Error(`Invalid package.json at ${pkgJsonPath}`);
3351
3416
  if (!pkgView.camstackAddons) throw new Error(`Package ${pkgView.name} has no camstack.addons manifest`);
3352
3417
  const targetDir = path$39.join(this.addonsDir, pkgView.name);
3353
- const strippedManifest = stripCamstackDeps(pkgView.raw);
3418
+ const strippedManifest = stripBundledDeps(pkgView.raw);
3354
3419
  await fs$17.promises.writeFile(pkgJsonPath, JSON.stringify(strippedManifest, null, 2));
3355
3420
  const strippedRuntimeDeps = strippedManifest["dependencies"];
3356
3421
  if (strippedRuntimeDeps != null && typeof strippedRuntimeDeps === "object" && Object.keys(strippedRuntimeDeps).length > 0) {
@@ -14,6 +14,34 @@ export declare function ensureDir(dirPath: string): void;
14
14
  * `fs.promises.cp` keeps the I/O off the event loop. (Node ≥18 stable.)
15
15
  */
16
16
  export declare function copyDirRecursive(src: string, dest: string): Promise<void>;
17
+ /**
18
+ * Runtime natives that must stay installable even when an addon forgets to
19
+ * declare them: the mirror of the build preset's `COMMON_NATIVE_DEPS`
20
+ * (tools/build/vite-lib.preset.ts — build tooling, not loadable at runtime).
21
+ * These are the ONLY packages a self-contained dist genuinely `require()`s
22
+ * from node_modules; everything else is already inlined by the bundle.
23
+ */
24
+ export declare const RUNTIME_NATIVE_KEEP: readonly string[];
25
+ /**
26
+ * Strip every dependency an addon's self-contained dist has already bundled,
27
+ * keeping ONLY what must resolve from node_modules at runtime:
28
+ *
29
+ * keep = RUNTIME_NATIVE_KEEP ∪ camstack.runtimeDependencies (minus @camstack/*)
30
+ *
31
+ * Addon dists are fully bundled (vite `externals: 'self-contained'`), yet the
32
+ * blanket `npm install` of the manifest's dependencies re-installed the whole
33
+ * tree the dist already contains — 98% of the deployed addons dir was
34
+ * redundant node_modules (74k files / 1.34 GB on the live hub; it stalled the
35
+ * backup walk and bloated every deploy). Kept PEER deps are folded into
36
+ * `dependencies` so `--omit=peer` installs can't drop them (pipeline's
37
+ * patched `werift`).
38
+ *
39
+ * Escape hatch: `CAMSTACK_INSTALL_FULL_DEPS=1` restores the legacy behaviour
40
+ * (strip only @camstack/*) for debugging a suspected undeclared runtime dep.
41
+ *
42
+ * Returns a new object (immutable).
43
+ */
44
+ export declare function stripBundledDeps(pkg: Record<string, unknown>): Record<string, unknown>;
17
45
  /**
18
46
  * Strip @camstack/* dependencies and devDependencies from a package.json object.
19
47
  * Used when installing addons into the addons directory — @camstack packages
@@ -4529,7 +4529,8 @@ var LocalChildClient = class {
4529
4529
  capName: msg.capName,
4530
4530
  method: msg.method,
4531
4531
  args: msg.args,
4532
- ...msg.deviceId !== void 0 ? { deviceId: msg.deviceId } : {}
4532
+ ...msg.deviceId !== void 0 ? { deviceId: msg.deviceId } : {},
4533
+ ...msg.addonId !== void 0 ? { addonId: msg.addonId } : {}
4533
4534
  });
4534
4535
  if (msg.kind === "addon-call") {
4535
4536
  if (this.addonCallHandler === null) throw new Error(`LocalChildClient: addon-call for "${msg.addonId}" arrived but no onAddonCall handler is registered`);
@@ -4527,7 +4527,8 @@ var LocalChildClient = class {
4527
4527
  capName: msg.capName,
4528
4528
  method: msg.method,
4529
4529
  args: msg.args,
4530
- ...msg.deviceId !== void 0 ? { deviceId: msg.deviceId } : {}
4530
+ ...msg.deviceId !== void 0 ? { deviceId: msg.deviceId } : {},
4531
+ ...msg.addonId !== void 0 ? { addonId: msg.addonId } : {}
4531
4532
  });
4532
4533
  if (msg.kind === "addon-call") {
4533
4534
  if (this.addonCallHandler === null) throw new Error(`LocalChildClient: addon-call for "${msg.addonId}" arrived but no onAddonCall handler is registered`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/system",
3
- "version": "1.1.38",
3
+ "version": "1.1.39",
4
4
  "description": "Core addon for CamStack — builtins, pipeline, process management, auth, logging, events",
5
5
  "keywords": [
6
6
  "camstack",