@camstack/system 1.2.3 → 1.2.4

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.
@@ -1396,7 +1396,11 @@ async function applyLinkedDevicesPatch(pctx, deviceId, patch) {
1396
1396
  //#region src/builtins/device-manager/device-store-sections.ts
1397
1397
  /** The device-manager's device-store-backed sections. Add an entry to extend. */
1398
1398
  var DEVICE_STORE_SECTIONS = [{
1399
- keys: [LINKED_MODE_CONFIG_KEY, LINKED_IDS_CONFIG_KEY],
1399
+ keys: [
1400
+ LINKED_MODE_CONFIG_KEY,
1401
+ LINKED_IDS_CONFIG_KEY,
1402
+ LINKED_TRACKED_IDS_CONFIG_KEY
1403
+ ],
1400
1404
  apply: (pctx, deviceId, patch) => applyLinkedDevicesPatch(pctx, deviceId, patch)
1401
1405
  }];
1402
1406
  /**
@@ -1391,7 +1391,11 @@ async function applyLinkedDevicesPatch(pctx, deviceId, patch) {
1391
1391
  //#region src/builtins/device-manager/device-store-sections.ts
1392
1392
  /** The device-manager's device-store-backed sections. Add an entry to extend. */
1393
1393
  var DEVICE_STORE_SECTIONS = [{
1394
- keys: [LINKED_MODE_CONFIG_KEY, LINKED_IDS_CONFIG_KEY],
1394
+ keys: [
1395
+ LINKED_MODE_CONFIG_KEY,
1396
+ LINKED_IDS_CONFIG_KEY,
1397
+ LINKED_TRACKED_IDS_CONFIG_KEY
1398
+ ],
1395
1399
  apply: (pctx, deviceId, patch) => applyLinkedDevicesPatch(pctx, deviceId, patch)
1396
1400
  }];
1397
1401
  /**
package/dist/index.d.ts CHANGED
@@ -85,4 +85,5 @@ export type { DirectCallerOptions } from './addon/addon-api-factory.js';
85
85
  export { IntegrationRegistry } from './builtins/sqlite-storage/integration-registry.js';
86
86
  export * from './kernel/index.js';
87
87
  export { runNpm, resolveNpmInvocation, type RunNpmOptions, type NpmInvocation, } from './kernel/deps/npm-command.js';
88
- export { ensureNativePrebuilds, TRADITIONAL_NATIVE_PACKAGES, type EnsureNativePrebuildsOptions, type PrebuildTarget, type PrebuildFetchFn, } from './kernel/deps/ensure-native-prebuilds.js';
88
+ export { ensureNativePrebuilds, hasDotNode, NATIVE_SCAN_DEPTH, TRADITIONAL_NATIVE_PACKAGES, type EnsureNativePrebuildsOptions, type PrebuildTarget, type PrebuildFetchFn, } from './kernel/deps/ensure-native-prebuilds.js';
89
+ export { ensureAddonNativePrebuilds, type EnsureAddonNativePrebuildsOptions, type NativeEnsureOutcome, type NativeEnsureResult, } from './kernel/deps/ensure-addon-natives.js';
package/dist/index.js CHANGED
@@ -3030,8 +3030,270 @@ var AddonManifest = class {
3030
3030
  }
3031
3031
  };
3032
3032
  //#endregion
3033
- //#region src/kernel/addon-installer.ts
3033
+ //#region src/kernel/deps/ensure-native-prebuilds.ts
3034
+ /**
3035
+ * `ensureNativePrebuilds` — the post-install native-prebuild ENSURE step the
3036
+ * shared `RootUpdateService` runs while staging a `@camstack/server` closure,
3037
+ * BEFORE its `findMissingNativePrebuilds` validator (in `@camstack/node-root`)
3038
+ * decides whether to arm the swap.
3039
+ *
3040
+ * Why this exists (Mac desktop incident, 2026-07-21): a runtime
3041
+ * `applyServerUpdate` on a packaged desktop stages the closure with
3042
+ * `npm install @camstack/server@X` under the app's BUNDLED bare Node. The one
3043
+ * TRADITIONAL (non-N-API) native, `better-sqlite3`, resolves its `.node` via
3044
+ * its `install` script `prebuild-install || node-gyp rebuild`. On a user's Mac
3045
+ * with no build toolchain (and where the auto `prebuild-install` does not
3046
+ * target the bundled Node's ABI/arch the way the DMG build's EXPLICIT
3047
+ * `prebuild-install --runtime node --target <bundled> --arch <arch>` call does)
3048
+ * that leaves the module SOURCE-ONLY → the closure has no `better-sqlite3`
3049
+ * `.node` → the validator refuses the swap ("staged closure missing native
3050
+ * prebuilds: better-sqlite3"). The docker image never hits this: its Node ABI
3051
+ * has a published prebuild the auto path fetches cleanly, and it carries a
3052
+ * build toolchain fallback.
3053
+ *
3054
+ * This mirrors the DMG build's explicit re-fetch (`scripts/stage-deps.mjs`)
3055
+ * into the runtime staging path: for each traditional native that is present
3056
+ * in the closure but missing its compiled `.node`, fetch the prebuild for the
3057
+ * RUNNING Node ABI/arch (the same Node that will load the swapped closure).
3058
+ *
3059
+ * N-API natives (`sharp`, `node-av`) are ABI-stable and ship their binaries as
3060
+ * platform SIBLING packages that npm fetches reliably with no compile step, so
3061
+ * they are not in scope here — a genuine miss there is a broken install the
3062
+ * validator should still reject, not something a prebuild re-fetch can heal.
3063
+ *
3064
+ * Injected into the update service from the construction site so the zero-dep
3065
+ * `@camstack/node-root` layer never depends on npm/@camstack/system, and the
3066
+ * fetch is unit-testable with a fake fetcher.
3067
+ */
3034
3068
  var execFileAsync$1 = (0, node_util.promisify)(node_child_process.execFile);
3069
+ /** Bounded recursive `.node` scan depth inside a module dir. */
3070
+ var NATIVE_SCAN_DEPTH = 4;
3071
+ /** Timeout for a single `prebuild-install` fetch. */
3072
+ var PREBUILD_FETCH_TIMEOUT_MS = 12e4;
3073
+ /**
3074
+ * Traditional (compile-or-prebuild) natives an installed closure must carry a
3075
+ * `.node` for. Only these are ENSURE-able by a prebuild re-fetch; N-API natives
3076
+ * are excluded (see file header).
3077
+ */
3078
+ var TRADITIONAL_NATIVE_PACKAGES = ["better-sqlite3"];
3079
+ /** Bounded recursive scan for ANY `*.node` file under `dir`. */
3080
+ function hasDotNode(dir, maxDepth) {
3081
+ let entries;
3082
+ try {
3083
+ entries = node_fs.readdirSync(dir, { withFileTypes: true });
3084
+ } catch {
3085
+ return false;
3086
+ }
3087
+ for (const entry of entries) if (entry.isFile() && entry.name.endsWith(".node")) return true;
3088
+ if (maxDepth <= 0) return false;
3089
+ for (const entry of entries) if (entry.isDirectory() && hasDotNode(node_path.join(dir, entry.name), maxDepth - 1)) return true;
3090
+ return false;
3091
+ }
3092
+ /** Locate the bundled `prebuild-install` bin.js inside the staged closure. */
3093
+ function resolvePrebuildInstallBin(closureDir, moduleDir) {
3094
+ return [node_path.join(moduleDir, "node_modules", "prebuild-install", "bin.js"), node_path.join(closureDir, "node_modules", "prebuild-install", "bin.js")].find((c) => node_fs.existsSync(c)) ?? null;
3095
+ }
3096
+ /**
3097
+ * Default fetcher: run the closure's own `prebuild-install` under the CURRENT
3098
+ * Node (`process.execPath`) — no reliance on `npx`/`npm` on PATH (a packaged
3099
+ * app ships a bare Node). Explicitly targets the running ABI/arch, mirroring
3100
+ * `scripts/stage-deps.mjs`.
3101
+ */
3102
+ function makeDefaultFetch(closureDir, logger) {
3103
+ return async (moduleDir, target) => {
3104
+ const bin = resolvePrebuildInstallBin(closureDir, moduleDir);
3105
+ if (bin === null) throw new Error(`prebuild-install not found in the staged closure for ${moduleDir}`);
3106
+ logger.info("fetching native prebuild via prebuild-install", { meta: {
3107
+ moduleDir,
3108
+ bin,
3109
+ nodeVersion: target.nodeVersion,
3110
+ arch: target.arch
3111
+ } });
3112
+ await execFileAsync$1(process.execPath, [
3113
+ bin,
3114
+ "--runtime",
3115
+ "node",
3116
+ "--target",
3117
+ target.nodeVersion,
3118
+ "--arch",
3119
+ target.arch
3120
+ ], {
3121
+ cwd: moduleDir,
3122
+ timeout: PREBUILD_FETCH_TIMEOUT_MS
3123
+ });
3124
+ };
3125
+ }
3126
+ /**
3127
+ * Ensure every traditional native in `<closureDir>/node_modules` carries its
3128
+ * compiled `.node`, fetching the prebuild for the running ABI when missing.
3129
+ *
3130
+ * A NO-OP when nothing is missing (docker / tests) and when a native package is
3131
+ * absent from the closure. Never throws for a single package's fetch failure —
3132
+ * it logs and continues so the downstream `findMissingNativePrebuilds`
3133
+ * validator makes the final swap/abort decision. Returns the packages whose
3134
+ * prebuild was successfully fetched (for logging/tests).
3135
+ */
3136
+ async function ensureNativePrebuilds(closureDir, options) {
3137
+ const nm = node_path.join(closureDir, "node_modules");
3138
+ const target = options.target ?? {
3139
+ nodeVersion: process.versions.node,
3140
+ arch: process.arch
3141
+ };
3142
+ const packages = options.packages ?? TRADITIONAL_NATIVE_PACKAGES;
3143
+ const fetchPrebuild = options.fetchPrebuild ?? makeDefaultFetch(closureDir, options.logger);
3144
+ const fetched = [];
3145
+ for (const pkg of packages) {
3146
+ const moduleDir = node_path.join(nm, pkg);
3147
+ if (!node_fs.existsSync(moduleDir)) continue;
3148
+ if (hasDotNode(moduleDir, 4)) continue;
3149
+ options.logger.warn("staged closure native prebuild missing — fetching for running ABI", { meta: {
3150
+ pkg,
3151
+ moduleDir,
3152
+ nodeVersion: target.nodeVersion,
3153
+ arch: target.arch
3154
+ } });
3155
+ try {
3156
+ await fetchPrebuild(moduleDir, target);
3157
+ } catch (err) {
3158
+ options.logger.error("native prebuild fetch failed (validator will decide)", { meta: {
3159
+ pkg,
3160
+ error: (0, _camstack_types_addon.errMsg)(err)
3161
+ } });
3162
+ continue;
3163
+ }
3164
+ if (hasDotNode(moduleDir, 4)) {
3165
+ fetched.push(pkg);
3166
+ options.logger.info("native prebuild fetched", { meta: { pkg } });
3167
+ } else options.logger.error("native prebuild still missing after fetch", { meta: { pkg } });
3168
+ }
3169
+ return fetched;
3170
+ }
3171
+ //#endregion
3172
+ //#region src/kernel/deps/ensure-addon-natives.ts
3173
+ /**
3174
+ * `ensureAddonNativePrebuilds` — the POST-install native-prebuild ENSURE pass an
3175
+ * addon install runs on the INSTALLED addon dir AFTER `npm install`.
3176
+ *
3177
+ * Why this exists (Mac desktop first-boot FATAL, 2026-07-21): `installCopy`
3178
+ * copies `dist/`, plants prebuilt natives from the bundled addon copy
3179
+ * (`copyBundledNativeModules`), then `npm install`s the addon's runtime deps.
3180
+ * On a TOOLCHAIN-LESS host that `npm install` RE-CREATES the traditional native
3181
+ * `better-sqlite3` SOURCE-ONLY — clobbering the good `.node` the pre-install
3182
+ * copy just planted — and the runner later dies at addon init with a cryptic
3183
+ * "Could not locate the bindings file". The copy-BEFORE-install fix was
3184
+ * empirically insufficient: npm's own install in the copied dir replaces the
3185
+ * satisfied dep without its binary.
3186
+ *
3187
+ * This pass runs LAST, so it always sees the final on-disk state npm left, and
3188
+ * per referenced traditional native still lacking its compiled `.node`:
3189
+ * 1. bundled-copy first — if the bundled addon copy carries the matching
3190
+ * `.node` (same machine → same platform/arch/ABI), re-plant it DIRECTLY.
3191
+ * Cheapest + offline-safe; runs BEFORE any network fetch.
3192
+ * 2. fetch fallback — otherwise fetch the prebuild for the running ABI via the
3193
+ * shared `ensureNativePrebuilds` pointed at the addon dir.
3194
+ * 3. FAIL LOUD — if the native is STILL source-only after both, log a clear
3195
+ * error naming module + addon and THROW so the install fails at install
3196
+ * time rather than at addon init.
3197
+ *
3198
+ * N-API natives (`sharp`, `node-av`) ship their binaries as ABI-stable platform
3199
+ * SIBLING packages npm fetches without a compile step, so they are excluded from
3200
+ * the default set — a miss there is a broken install, not something a prebuild
3201
+ * re-fetch heals (same rationale as `ensureNativePrebuilds`).
3202
+ */
3203
+ /**
3204
+ * Re-plant `<srcModuleDir>` → `<dstModuleDir>` when the source carries a `.node`.
3205
+ * Returns whether the destination ends up with a compiled binary.
3206
+ */
3207
+ function replantFromBundled(srcModuleDir, dstModuleDir, hasNative) {
3208
+ if (!hasNative(srcModuleDir)) return false;
3209
+ try {
3210
+ node_fs.rmSync(dstModuleDir, {
3211
+ recursive: true,
3212
+ force: true
3213
+ });
3214
+ node_fs.mkdirSync(node_path.dirname(dstModuleDir), { recursive: true });
3215
+ node_fs.cpSync(srcModuleDir, dstModuleDir, {
3216
+ recursive: true,
3217
+ dereference: true
3218
+ });
3219
+ } catch {
3220
+ return false;
3221
+ }
3222
+ return hasNative(dstModuleDir);
3223
+ }
3224
+ /**
3225
+ * Ensure every referenced traditional native in `<addonDir>/node_modules`
3226
+ * carries its compiled `.node` after the addon's `npm install`. Bundled-copy
3227
+ * first (offline), prebuild-fetch fallback, LOUD throw on a residual miss.
3228
+ *
3229
+ * SKIPS a native absent from the addon's own `node_modules` (it resolves via a
3230
+ * hoisted host copy — not this addon's concern). A NO-OP in docker / dev where
3231
+ * npm produced a working `.node` (already-present). Returns the per-native
3232
+ * outcomes for logging/tests.
3233
+ *
3234
+ * @throws if any required native is still source-only after copy + fetch.
3235
+ */
3236
+ async function ensureAddonNativePrebuilds(addonDir, options) {
3237
+ const packages = options.packages ?? TRADITIONAL_NATIVE_PACKAGES;
3238
+ const hasNative = options.hasNative ?? ((dir) => hasDotNode(dir, 4));
3239
+ const nm = node_path.join(addonDir, "node_modules");
3240
+ const results = [];
3241
+ const missing = [];
3242
+ for (const pkg of packages) {
3243
+ const moduleDir = node_path.join(nm, pkg);
3244
+ if (!node_fs.existsSync(moduleDir)) continue;
3245
+ if (hasNative(moduleDir)) {
3246
+ results.push({
3247
+ pkg,
3248
+ outcome: "already-present"
3249
+ });
3250
+ continue;
3251
+ }
3252
+ if (options.bundledSourceDir) {
3253
+ const src = node_path.join(options.bundledSourceDir, "node_modules", pkg);
3254
+ if (replantFromBundled(src, moduleDir, hasNative)) {
3255
+ options.logger.info("addon native prebuild re-planted from bundled copy (offline)", { meta: {
3256
+ addon: options.addonName,
3257
+ pkg,
3258
+ src,
3259
+ moduleDir
3260
+ } });
3261
+ results.push({
3262
+ pkg,
3263
+ outcome: "copied"
3264
+ });
3265
+ continue;
3266
+ }
3267
+ }
3268
+ if ((await ensureNativePrebuilds(addonDir, {
3269
+ logger: options.logger,
3270
+ packages: [pkg],
3271
+ ...options.target ? { target: options.target } : {},
3272
+ ...options.fetchPrebuild ? { fetchPrebuild: options.fetchPrebuild } : {}
3273
+ })).includes(pkg)) {
3274
+ results.push({
3275
+ pkg,
3276
+ outcome: "fetched"
3277
+ });
3278
+ continue;
3279
+ }
3280
+ options.logger.error("addon native prebuild MISSING after ensure — module has no compiled .node", { meta: {
3281
+ addon: options.addonName,
3282
+ pkg,
3283
+ moduleDir
3284
+ } });
3285
+ results.push({
3286
+ pkg,
3287
+ outcome: "missing"
3288
+ });
3289
+ missing.push(pkg);
3290
+ }
3291
+ if (missing.length > 0) throw new Error(`${options.addonName} — required native module(s) missing a compiled .node after install (bundled-copy + prebuild-fetch both failed): ${missing.join(", ")}`);
3292
+ return results;
3293
+ }
3294
+ //#endregion
3295
+ //#region src/kernel/addon-installer.ts
3296
+ var execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
3035
3297
  function parseInstallSource(value) {
3036
3298
  switch (value) {
3037
3299
  case "npm":
@@ -3340,6 +3602,11 @@ var AddonInstaller = class AddonInstaller {
3340
3602
  } catch (err) {
3341
3603
  this.logger.warn(`${packageName} — native deps install failed (continuing)`, { meta: { error: require_dist.errMsg(err) } });
3342
3604
  }
3605
+ await ensureAddonNativePrebuilds(targetDir, {
3606
+ addonName: packageName,
3607
+ logger: this.logger,
3608
+ bundledSourceDir: sourceDir
3609
+ });
3343
3610
  this.logger.info(`${packageName} — copied from ${sourceDir}`);
3344
3611
  return {
3345
3612
  name: pkgData.name,
@@ -3489,7 +3756,7 @@ var AddonInstaller = class AddonInstaller {
3489
3756
  const stagingDir = node_path.join(stagingRoot, `install-${process.pid}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`);
3490
3757
  ensureDir(stagingDir);
3491
3758
  try {
3492
- await execFileAsync$1("tar", [
3759
+ await execFileAsync("tar", [
3493
3760
  "-xzf",
3494
3761
  tgzPath,
3495
3762
  "-C",
@@ -3526,6 +3793,10 @@ var AddonInstaller = class AddonInstaller {
3526
3793
  } catch (nativeErr) {
3527
3794
  throw new Error(`${pkgView.name} — native deps install failed: ${require_dist.errMsg(nativeErr)}`, { cause: nativeErr });
3528
3795
  }
3796
+ await ensureAddonNativePrebuilds(pkgDir, {
3797
+ addonName: pkgView.name,
3798
+ logger: this.logger
3799
+ });
3529
3800
  await this.evictInstallDir(targetDir);
3530
3801
  ensureDir(node_path.dirname(targetDir));
3531
3802
  await node_fs.promises.rename(pkgDir, targetDir);
@@ -80144,7 +80415,7 @@ var require_compression = /* @__PURE__ */ require_chunk.__commonJSMin(((exports,
80144
80415
  var { defaultsDeep } = require_lodash();
80145
80416
  var { parseByteString } = require_utils$3();
80146
80417
  var zlib = require("zlib");
80147
- var { promisify: promisify$1 } = require("util");
80418
+ var { promisify } = require("util");
80148
80419
  /**
80149
80420
  * This is a transmission compression middleware. It supports
80150
80421
  * the `deflate`, `deflateRaw` & `gzip` compression methods.
@@ -80162,16 +80433,16 @@ var require_compression = /* @__PURE__ */ require_chunk.__commonJSMin(((exports,
80162
80433
  const threshold = parseByteString(opts.threshold);
80163
80434
  switch (opts.method) {
80164
80435
  case "deflate":
80165
- compress = promisify$1(zlib.deflate);
80166
- decompress = promisify$1(zlib.inflate);
80436
+ compress = promisify(zlib.deflate);
80437
+ decompress = promisify(zlib.inflate);
80167
80438
  break;
80168
80439
  case "deflateRaw":
80169
- compress = promisify$1(zlib.deflateRaw);
80170
- decompress = promisify$1(zlib.inflateRaw);
80440
+ compress = promisify(zlib.deflateRaw);
80441
+ decompress = promisify(zlib.inflateRaw);
80171
80442
  break;
80172
80443
  case "gzip":
80173
- compress = promisify$1(zlib.gzip);
80174
- decompress = promisify$1(zlib.gunzip);
80444
+ compress = promisify(zlib.gzip);
80445
+ decompress = promisify(zlib.gunzip);
80175
80446
  break;
80176
80447
  default:
80177
80448
  /* istanbul ignore next */
@@ -93398,145 +93669,6 @@ var LifecycleJobEngine = class {
93398
93669
  }
93399
93670
  };
93400
93671
  //#endregion
93401
- //#region src/kernel/deps/ensure-native-prebuilds.ts
93402
- /**
93403
- * `ensureNativePrebuilds` — the post-install native-prebuild ENSURE step the
93404
- * shared `RootUpdateService` runs while staging a `@camstack/server` closure,
93405
- * BEFORE its `findMissingNativePrebuilds` validator (in `@camstack/node-root`)
93406
- * decides whether to arm the swap.
93407
- *
93408
- * Why this exists (Mac desktop incident, 2026-07-21): a runtime
93409
- * `applyServerUpdate` on a packaged desktop stages the closure with
93410
- * `npm install @camstack/server@X` under the app's BUNDLED bare Node. The one
93411
- * TRADITIONAL (non-N-API) native, `better-sqlite3`, resolves its `.node` via
93412
- * its `install` script `prebuild-install || node-gyp rebuild`. On a user's Mac
93413
- * with no build toolchain (and where the auto `prebuild-install` does not
93414
- * target the bundled Node's ABI/arch the way the DMG build's EXPLICIT
93415
- * `prebuild-install --runtime node --target <bundled> --arch <arch>` call does)
93416
- * that leaves the module SOURCE-ONLY → the closure has no `better-sqlite3`
93417
- * `.node` → the validator refuses the swap ("staged closure missing native
93418
- * prebuilds: better-sqlite3"). The docker image never hits this: its Node ABI
93419
- * has a published prebuild the auto path fetches cleanly, and it carries a
93420
- * build toolchain fallback.
93421
- *
93422
- * This mirrors the DMG build's explicit re-fetch (`scripts/stage-deps.mjs`)
93423
- * into the runtime staging path: for each traditional native that is present
93424
- * in the closure but missing its compiled `.node`, fetch the prebuild for the
93425
- * RUNNING Node ABI/arch (the same Node that will load the swapped closure).
93426
- *
93427
- * N-API natives (`sharp`, `node-av`) are ABI-stable and ship their binaries as
93428
- * platform SIBLING packages that npm fetches reliably with no compile step, so
93429
- * they are not in scope here — a genuine miss there is a broken install the
93430
- * validator should still reject, not something a prebuild re-fetch can heal.
93431
- *
93432
- * Injected into the update service from the construction site so the zero-dep
93433
- * `@camstack/node-root` layer never depends on npm/@camstack/system, and the
93434
- * fetch is unit-testable with a fake fetcher.
93435
- */
93436
- var execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
93437
- /** Bounded recursive `.node` scan depth inside a module dir. */
93438
- var NATIVE_SCAN_DEPTH = 4;
93439
- /** Timeout for a single `prebuild-install` fetch. */
93440
- var PREBUILD_FETCH_TIMEOUT_MS = 12e4;
93441
- /**
93442
- * Traditional (compile-or-prebuild) natives an installed closure must carry a
93443
- * `.node` for. Only these are ENSURE-able by a prebuild re-fetch; N-API natives
93444
- * are excluded (see file header).
93445
- */
93446
- var TRADITIONAL_NATIVE_PACKAGES = ["better-sqlite3"];
93447
- /** Bounded recursive scan for ANY `*.node` file under `dir`. */
93448
- function hasDotNode(dir, maxDepth) {
93449
- let entries;
93450
- try {
93451
- entries = node_fs.readdirSync(dir, { withFileTypes: true });
93452
- } catch {
93453
- return false;
93454
- }
93455
- for (const entry of entries) if (entry.isFile() && entry.name.endsWith(".node")) return true;
93456
- if (maxDepth <= 0) return false;
93457
- for (const entry of entries) if (entry.isDirectory() && hasDotNode(node_path.join(dir, entry.name), maxDepth - 1)) return true;
93458
- return false;
93459
- }
93460
- /** Locate the bundled `prebuild-install` bin.js inside the staged closure. */
93461
- function resolvePrebuildInstallBin(closureDir, moduleDir) {
93462
- return [node_path.join(moduleDir, "node_modules", "prebuild-install", "bin.js"), node_path.join(closureDir, "node_modules", "prebuild-install", "bin.js")].find((c) => node_fs.existsSync(c)) ?? null;
93463
- }
93464
- /**
93465
- * Default fetcher: run the closure's own `prebuild-install` under the CURRENT
93466
- * Node (`process.execPath`) — no reliance on `npx`/`npm` on PATH (a packaged
93467
- * app ships a bare Node). Explicitly targets the running ABI/arch, mirroring
93468
- * `scripts/stage-deps.mjs`.
93469
- */
93470
- function makeDefaultFetch(closureDir, logger) {
93471
- return async (moduleDir, target) => {
93472
- const bin = resolvePrebuildInstallBin(closureDir, moduleDir);
93473
- if (bin === null) throw new Error(`prebuild-install not found in the staged closure for ${moduleDir}`);
93474
- logger.info("fetching native prebuild via prebuild-install", { meta: {
93475
- moduleDir,
93476
- bin,
93477
- nodeVersion: target.nodeVersion,
93478
- arch: target.arch
93479
- } });
93480
- await execFileAsync(process.execPath, [
93481
- bin,
93482
- "--runtime",
93483
- "node",
93484
- "--target",
93485
- target.nodeVersion,
93486
- "--arch",
93487
- target.arch
93488
- ], {
93489
- cwd: moduleDir,
93490
- timeout: PREBUILD_FETCH_TIMEOUT_MS
93491
- });
93492
- };
93493
- }
93494
- /**
93495
- * Ensure every traditional native in `<closureDir>/node_modules` carries its
93496
- * compiled `.node`, fetching the prebuild for the running ABI when missing.
93497
- *
93498
- * A NO-OP when nothing is missing (docker / tests) and when a native package is
93499
- * absent from the closure. Never throws for a single package's fetch failure —
93500
- * it logs and continues so the downstream `findMissingNativePrebuilds`
93501
- * validator makes the final swap/abort decision. Returns the packages whose
93502
- * prebuild was successfully fetched (for logging/tests).
93503
- */
93504
- async function ensureNativePrebuilds(closureDir, options) {
93505
- const nm = node_path.join(closureDir, "node_modules");
93506
- const target = options.target ?? {
93507
- nodeVersion: process.versions.node,
93508
- arch: process.arch
93509
- };
93510
- const packages = options.packages ?? TRADITIONAL_NATIVE_PACKAGES;
93511
- const fetchPrebuild = options.fetchPrebuild ?? makeDefaultFetch(closureDir, options.logger);
93512
- const fetched = [];
93513
- for (const pkg of packages) {
93514
- const moduleDir = node_path.join(nm, pkg);
93515
- if (!node_fs.existsSync(moduleDir)) continue;
93516
- if (hasDotNode(moduleDir, NATIVE_SCAN_DEPTH)) continue;
93517
- options.logger.warn("staged closure native prebuild missing — fetching for running ABI", { meta: {
93518
- pkg,
93519
- moduleDir,
93520
- nodeVersion: target.nodeVersion,
93521
- arch: target.arch
93522
- } });
93523
- try {
93524
- await fetchPrebuild(moduleDir, target);
93525
- } catch (err) {
93526
- options.logger.error("native prebuild fetch failed (validator will decide)", { meta: {
93527
- pkg,
93528
- error: (0, _camstack_types_addon.errMsg)(err)
93529
- } });
93530
- continue;
93531
- }
93532
- if (hasDotNode(moduleDir, NATIVE_SCAN_DEPTH)) {
93533
- fetched.push(pkg);
93534
- options.logger.info("native prebuild fetched", { meta: { pkg } });
93535
- } else options.logger.error("native prebuild still missing after fetch", { meta: { pkg } });
93536
- }
93537
- return fetched;
93538
- }
93539
- //#endregion
93540
93672
  exports.AGENT_CAP_FWD_ACTION = require_manifest_python_deps.AGENT_CAP_FWD_ACTION;
93541
93673
  exports.AGENT_CAP_FWD_SERVICE = require_manifest_python_deps.AGENT_CAP_FWD_SERVICE;
93542
93674
  exports.AGENT_READINESS_SERVICE_NAME = AGENT_READINESS_SERVICE_NAME;
@@ -93603,6 +93735,7 @@ exports.LogRingBuffer = LogRingBuffer;
93603
93735
  exports.METHOD_ACCESS_MAP = require_dist.METHOD_ACCESS_MAP;
93604
93736
  exports.ModelDownloadService = require_model_download_service.ModelDownloadService;
93605
93737
  exports.NATIVE_PROVIDER_SERVICE_INFIX = require_manifest_python_deps.NATIVE_PROVIDER_SERVICE_INFIX;
93738
+ exports.NATIVE_SCAN_DEPTH = NATIVE_SCAN_DEPTH;
93606
93739
  exports.NativeMetricsAddon = require_builtins_native_metrics_native_metrics_addon.default;
93607
93740
  exports.NativeMetricsProvider = require_builtins_native_metrics_native_metrics_addon.NativeMetricsProvider;
93608
93741
  exports.NetworkQualityTracker = NetworkQualityTracker;
@@ -93712,6 +93845,7 @@ exports.downloadFile = require_model_download_service.downloadFile;
93712
93845
  exports.downloadModel = require_model_download_service.downloadModel;
93713
93846
  exports.emitDownForOwnedCaps = require_dist.emitDownForOwnedCaps;
93714
93847
  exports.encodeFrame = require_manifest_python_deps.encodeFrame;
93848
+ exports.ensureAddonNativePrebuilds = ensureAddonNativePrebuilds;
93715
93849
  Object.defineProperty(exports, "ensureBinary", {
93716
93850
  enumerable: true,
93717
93851
  get: function() {
@@ -93772,6 +93906,7 @@ Object.defineProperty(exports, "getPythonDownloadUrl", {
93772
93906
  exports.getRestartMarkerPath = getRestartMarkerPath;
93773
93907
  exports.getSinglePidStats = require_resource_monitor.getSinglePidStats;
93774
93908
  exports.getWorkerDeviceRegistry = require_manifest_python_deps.getWorkerDeviceRegistry;
93909
+ exports.hasDotNode = hasDotNode;
93775
93910
  exports.hashClusterSecret = hashClusterSecret;
93776
93911
  exports.installManifestNativeDeps = require_manifest_python_deps.installManifestNativeDeps;
93777
93912
  exports.installManifestPythonDeps = require_manifest_python_deps.installManifestPythonDeps;
package/dist/index.mjs CHANGED
@@ -3022,8 +3022,270 @@ var AddonManifest = class {
3022
3022
  }
3023
3023
  };
3024
3024
  //#endregion
3025
- //#region src/kernel/addon-installer.ts
3025
+ //#region src/kernel/deps/ensure-native-prebuilds.ts
3026
+ /**
3027
+ * `ensureNativePrebuilds` — the post-install native-prebuild ENSURE step the
3028
+ * shared `RootUpdateService` runs while staging a `@camstack/server` closure,
3029
+ * BEFORE its `findMissingNativePrebuilds` validator (in `@camstack/node-root`)
3030
+ * decides whether to arm the swap.
3031
+ *
3032
+ * Why this exists (Mac desktop incident, 2026-07-21): a runtime
3033
+ * `applyServerUpdate` on a packaged desktop stages the closure with
3034
+ * `npm install @camstack/server@X` under the app's BUNDLED bare Node. The one
3035
+ * TRADITIONAL (non-N-API) native, `better-sqlite3`, resolves its `.node` via
3036
+ * its `install` script `prebuild-install || node-gyp rebuild`. On a user's Mac
3037
+ * with no build toolchain (and where the auto `prebuild-install` does not
3038
+ * target the bundled Node's ABI/arch the way the DMG build's EXPLICIT
3039
+ * `prebuild-install --runtime node --target <bundled> --arch <arch>` call does)
3040
+ * that leaves the module SOURCE-ONLY → the closure has no `better-sqlite3`
3041
+ * `.node` → the validator refuses the swap ("staged closure missing native
3042
+ * prebuilds: better-sqlite3"). The docker image never hits this: its Node ABI
3043
+ * has a published prebuild the auto path fetches cleanly, and it carries a
3044
+ * build toolchain fallback.
3045
+ *
3046
+ * This mirrors the DMG build's explicit re-fetch (`scripts/stage-deps.mjs`)
3047
+ * into the runtime staging path: for each traditional native that is present
3048
+ * in the closure but missing its compiled `.node`, fetch the prebuild for the
3049
+ * RUNNING Node ABI/arch (the same Node that will load the swapped closure).
3050
+ *
3051
+ * N-API natives (`sharp`, `node-av`) are ABI-stable and ship their binaries as
3052
+ * platform SIBLING packages that npm fetches reliably with no compile step, so
3053
+ * they are not in scope here — a genuine miss there is a broken install the
3054
+ * validator should still reject, not something a prebuild re-fetch can heal.
3055
+ *
3056
+ * Injected into the update service from the construction site so the zero-dep
3057
+ * `@camstack/node-root` layer never depends on npm/@camstack/system, and the
3058
+ * fetch is unit-testable with a fake fetcher.
3059
+ */
3026
3060
  var execFileAsync$1 = promisify(execFile);
3061
+ /** Bounded recursive `.node` scan depth inside a module dir. */
3062
+ var NATIVE_SCAN_DEPTH = 4;
3063
+ /** Timeout for a single `prebuild-install` fetch. */
3064
+ var PREBUILD_FETCH_TIMEOUT_MS = 12e4;
3065
+ /**
3066
+ * Traditional (compile-or-prebuild) natives an installed closure must carry a
3067
+ * `.node` for. Only these are ENSURE-able by a prebuild re-fetch; N-API natives
3068
+ * are excluded (see file header).
3069
+ */
3070
+ var TRADITIONAL_NATIVE_PACKAGES = ["better-sqlite3"];
3071
+ /** Bounded recursive scan for ANY `*.node` file under `dir`. */
3072
+ function hasDotNode(dir, maxDepth) {
3073
+ let entries;
3074
+ try {
3075
+ entries = fs$17.readdirSync(dir, { withFileTypes: true });
3076
+ } catch {
3077
+ return false;
3078
+ }
3079
+ for (const entry of entries) if (entry.isFile() && entry.name.endsWith(".node")) return true;
3080
+ if (maxDepth <= 0) return false;
3081
+ for (const entry of entries) if (entry.isDirectory() && hasDotNode(path$39.join(dir, entry.name), maxDepth - 1)) return true;
3082
+ return false;
3083
+ }
3084
+ /** Locate the bundled `prebuild-install` bin.js inside the staged closure. */
3085
+ function resolvePrebuildInstallBin(closureDir, moduleDir) {
3086
+ return [path$39.join(moduleDir, "node_modules", "prebuild-install", "bin.js"), path$39.join(closureDir, "node_modules", "prebuild-install", "bin.js")].find((c) => fs$17.existsSync(c)) ?? null;
3087
+ }
3088
+ /**
3089
+ * Default fetcher: run the closure's own `prebuild-install` under the CURRENT
3090
+ * Node (`process.execPath`) — no reliance on `npx`/`npm` on PATH (a packaged
3091
+ * app ships a bare Node). Explicitly targets the running ABI/arch, mirroring
3092
+ * `scripts/stage-deps.mjs`.
3093
+ */
3094
+ function makeDefaultFetch(closureDir, logger) {
3095
+ return async (moduleDir, target) => {
3096
+ const bin = resolvePrebuildInstallBin(closureDir, moduleDir);
3097
+ if (bin === null) throw new Error(`prebuild-install not found in the staged closure for ${moduleDir}`);
3098
+ logger.info("fetching native prebuild via prebuild-install", { meta: {
3099
+ moduleDir,
3100
+ bin,
3101
+ nodeVersion: target.nodeVersion,
3102
+ arch: target.arch
3103
+ } });
3104
+ await execFileAsync$1(process.execPath, [
3105
+ bin,
3106
+ "--runtime",
3107
+ "node",
3108
+ "--target",
3109
+ target.nodeVersion,
3110
+ "--arch",
3111
+ target.arch
3112
+ ], {
3113
+ cwd: moduleDir,
3114
+ timeout: PREBUILD_FETCH_TIMEOUT_MS
3115
+ });
3116
+ };
3117
+ }
3118
+ /**
3119
+ * Ensure every traditional native in `<closureDir>/node_modules` carries its
3120
+ * compiled `.node`, fetching the prebuild for the running ABI when missing.
3121
+ *
3122
+ * A NO-OP when nothing is missing (docker / tests) and when a native package is
3123
+ * absent from the closure. Never throws for a single package's fetch failure —
3124
+ * it logs and continues so the downstream `findMissingNativePrebuilds`
3125
+ * validator makes the final swap/abort decision. Returns the packages whose
3126
+ * prebuild was successfully fetched (for logging/tests).
3127
+ */
3128
+ async function ensureNativePrebuilds(closureDir, options) {
3129
+ const nm = path$39.join(closureDir, "node_modules");
3130
+ const target = options.target ?? {
3131
+ nodeVersion: process.versions.node,
3132
+ arch: process.arch
3133
+ };
3134
+ const packages = options.packages ?? TRADITIONAL_NATIVE_PACKAGES;
3135
+ const fetchPrebuild = options.fetchPrebuild ?? makeDefaultFetch(closureDir, options.logger);
3136
+ const fetched = [];
3137
+ for (const pkg of packages) {
3138
+ const moduleDir = path$39.join(nm, pkg);
3139
+ if (!fs$17.existsSync(moduleDir)) continue;
3140
+ if (hasDotNode(moduleDir, 4)) continue;
3141
+ options.logger.warn("staged closure native prebuild missing — fetching for running ABI", { meta: {
3142
+ pkg,
3143
+ moduleDir,
3144
+ nodeVersion: target.nodeVersion,
3145
+ arch: target.arch
3146
+ } });
3147
+ try {
3148
+ await fetchPrebuild(moduleDir, target);
3149
+ } catch (err) {
3150
+ options.logger.error("native prebuild fetch failed (validator will decide)", { meta: {
3151
+ pkg,
3152
+ error: errMsg(err)
3153
+ } });
3154
+ continue;
3155
+ }
3156
+ if (hasDotNode(moduleDir, 4)) {
3157
+ fetched.push(pkg);
3158
+ options.logger.info("native prebuild fetched", { meta: { pkg } });
3159
+ } else options.logger.error("native prebuild still missing after fetch", { meta: { pkg } });
3160
+ }
3161
+ return fetched;
3162
+ }
3163
+ //#endregion
3164
+ //#region src/kernel/deps/ensure-addon-natives.ts
3165
+ /**
3166
+ * `ensureAddonNativePrebuilds` — the POST-install native-prebuild ENSURE pass an
3167
+ * addon install runs on the INSTALLED addon dir AFTER `npm install`.
3168
+ *
3169
+ * Why this exists (Mac desktop first-boot FATAL, 2026-07-21): `installCopy`
3170
+ * copies `dist/`, plants prebuilt natives from the bundled addon copy
3171
+ * (`copyBundledNativeModules`), then `npm install`s the addon's runtime deps.
3172
+ * On a TOOLCHAIN-LESS host that `npm install` RE-CREATES the traditional native
3173
+ * `better-sqlite3` SOURCE-ONLY — clobbering the good `.node` the pre-install
3174
+ * copy just planted — and the runner later dies at addon init with a cryptic
3175
+ * "Could not locate the bindings file". The copy-BEFORE-install fix was
3176
+ * empirically insufficient: npm's own install in the copied dir replaces the
3177
+ * satisfied dep without its binary.
3178
+ *
3179
+ * This pass runs LAST, so it always sees the final on-disk state npm left, and
3180
+ * per referenced traditional native still lacking its compiled `.node`:
3181
+ * 1. bundled-copy first — if the bundled addon copy carries the matching
3182
+ * `.node` (same machine → same platform/arch/ABI), re-plant it DIRECTLY.
3183
+ * Cheapest + offline-safe; runs BEFORE any network fetch.
3184
+ * 2. fetch fallback — otherwise fetch the prebuild for the running ABI via the
3185
+ * shared `ensureNativePrebuilds` pointed at the addon dir.
3186
+ * 3. FAIL LOUD — if the native is STILL source-only after both, log a clear
3187
+ * error naming module + addon and THROW so the install fails at install
3188
+ * time rather than at addon init.
3189
+ *
3190
+ * N-API natives (`sharp`, `node-av`) ship their binaries as ABI-stable platform
3191
+ * SIBLING packages npm fetches without a compile step, so they are excluded from
3192
+ * the default set — a miss there is a broken install, not something a prebuild
3193
+ * re-fetch heals (same rationale as `ensureNativePrebuilds`).
3194
+ */
3195
+ /**
3196
+ * Re-plant `<srcModuleDir>` → `<dstModuleDir>` when the source carries a `.node`.
3197
+ * Returns whether the destination ends up with a compiled binary.
3198
+ */
3199
+ function replantFromBundled(srcModuleDir, dstModuleDir, hasNative) {
3200
+ if (!hasNative(srcModuleDir)) return false;
3201
+ try {
3202
+ fs$17.rmSync(dstModuleDir, {
3203
+ recursive: true,
3204
+ force: true
3205
+ });
3206
+ fs$17.mkdirSync(path$39.dirname(dstModuleDir), { recursive: true });
3207
+ fs$17.cpSync(srcModuleDir, dstModuleDir, {
3208
+ recursive: true,
3209
+ dereference: true
3210
+ });
3211
+ } catch {
3212
+ return false;
3213
+ }
3214
+ return hasNative(dstModuleDir);
3215
+ }
3216
+ /**
3217
+ * Ensure every referenced traditional native in `<addonDir>/node_modules`
3218
+ * carries its compiled `.node` after the addon's `npm install`. Bundled-copy
3219
+ * first (offline), prebuild-fetch fallback, LOUD throw on a residual miss.
3220
+ *
3221
+ * SKIPS a native absent from the addon's own `node_modules` (it resolves via a
3222
+ * hoisted host copy — not this addon's concern). A NO-OP in docker / dev where
3223
+ * npm produced a working `.node` (already-present). Returns the per-native
3224
+ * outcomes for logging/tests.
3225
+ *
3226
+ * @throws if any required native is still source-only after copy + fetch.
3227
+ */
3228
+ async function ensureAddonNativePrebuilds(addonDir, options) {
3229
+ const packages = options.packages ?? TRADITIONAL_NATIVE_PACKAGES;
3230
+ const hasNative = options.hasNative ?? ((dir) => hasDotNode(dir, 4));
3231
+ const nm = path$39.join(addonDir, "node_modules");
3232
+ const results = [];
3233
+ const missing = [];
3234
+ for (const pkg of packages) {
3235
+ const moduleDir = path$39.join(nm, pkg);
3236
+ if (!fs$17.existsSync(moduleDir)) continue;
3237
+ if (hasNative(moduleDir)) {
3238
+ results.push({
3239
+ pkg,
3240
+ outcome: "already-present"
3241
+ });
3242
+ continue;
3243
+ }
3244
+ if (options.bundledSourceDir) {
3245
+ const src = path$39.join(options.bundledSourceDir, "node_modules", pkg);
3246
+ if (replantFromBundled(src, moduleDir, hasNative)) {
3247
+ options.logger.info("addon native prebuild re-planted from bundled copy (offline)", { meta: {
3248
+ addon: options.addonName,
3249
+ pkg,
3250
+ src,
3251
+ moduleDir
3252
+ } });
3253
+ results.push({
3254
+ pkg,
3255
+ outcome: "copied"
3256
+ });
3257
+ continue;
3258
+ }
3259
+ }
3260
+ if ((await ensureNativePrebuilds(addonDir, {
3261
+ logger: options.logger,
3262
+ packages: [pkg],
3263
+ ...options.target ? { target: options.target } : {},
3264
+ ...options.fetchPrebuild ? { fetchPrebuild: options.fetchPrebuild } : {}
3265
+ })).includes(pkg)) {
3266
+ results.push({
3267
+ pkg,
3268
+ outcome: "fetched"
3269
+ });
3270
+ continue;
3271
+ }
3272
+ options.logger.error("addon native prebuild MISSING after ensure — module has no compiled .node", { meta: {
3273
+ addon: options.addonName,
3274
+ pkg,
3275
+ moduleDir
3276
+ } });
3277
+ results.push({
3278
+ pkg,
3279
+ outcome: "missing"
3280
+ });
3281
+ missing.push(pkg);
3282
+ }
3283
+ if (missing.length > 0) throw new Error(`${options.addonName} — required native module(s) missing a compiled .node after install (bundled-copy + prebuild-fetch both failed): ${missing.join(", ")}`);
3284
+ return results;
3285
+ }
3286
+ //#endregion
3287
+ //#region src/kernel/addon-installer.ts
3288
+ var execFileAsync = promisify(execFile);
3027
3289
  function parseInstallSource(value) {
3028
3290
  switch (value) {
3029
3291
  case "npm":
@@ -3332,6 +3594,11 @@ var AddonInstaller = class AddonInstaller {
3332
3594
  } catch (err) {
3333
3595
  this.logger.warn(`${packageName} — native deps install failed (continuing)`, { meta: { error: errMsg$1(err) } });
3334
3596
  }
3597
+ await ensureAddonNativePrebuilds(targetDir, {
3598
+ addonName: packageName,
3599
+ logger: this.logger,
3600
+ bundledSourceDir: sourceDir
3601
+ });
3335
3602
  this.logger.info(`${packageName} — copied from ${sourceDir}`);
3336
3603
  return {
3337
3604
  name: pkgData.name,
@@ -3481,7 +3748,7 @@ var AddonInstaller = class AddonInstaller {
3481
3748
  const stagingDir = path$39.join(stagingRoot, `install-${process.pid}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`);
3482
3749
  ensureDir(stagingDir);
3483
3750
  try {
3484
- await execFileAsync$1("tar", [
3751
+ await execFileAsync("tar", [
3485
3752
  "-xzf",
3486
3753
  tgzPath,
3487
3754
  "-C",
@@ -3518,6 +3785,10 @@ var AddonInstaller = class AddonInstaller {
3518
3785
  } catch (nativeErr) {
3519
3786
  throw new Error(`${pkgView.name} — native deps install failed: ${errMsg$1(nativeErr)}`, { cause: nativeErr });
3520
3787
  }
3788
+ await ensureAddonNativePrebuilds(pkgDir, {
3789
+ addonName: pkgView.name,
3790
+ logger: this.logger
3791
+ });
3521
3792
  await this.evictInstallDir(targetDir);
3522
3793
  ensureDir(path$39.dirname(targetDir));
3523
3794
  await fs$17.promises.rename(pkgDir, targetDir);
@@ -93390,143 +93661,4 @@ var LifecycleJobEngine = class {
93390
93661
  }
93391
93662
  };
93392
93663
  //#endregion
93393
- //#region src/kernel/deps/ensure-native-prebuilds.ts
93394
- /**
93395
- * `ensureNativePrebuilds` — the post-install native-prebuild ENSURE step the
93396
- * shared `RootUpdateService` runs while staging a `@camstack/server` closure,
93397
- * BEFORE its `findMissingNativePrebuilds` validator (in `@camstack/node-root`)
93398
- * decides whether to arm the swap.
93399
- *
93400
- * Why this exists (Mac desktop incident, 2026-07-21): a runtime
93401
- * `applyServerUpdate` on a packaged desktop stages the closure with
93402
- * `npm install @camstack/server@X` under the app's BUNDLED bare Node. The one
93403
- * TRADITIONAL (non-N-API) native, `better-sqlite3`, resolves its `.node` via
93404
- * its `install` script `prebuild-install || node-gyp rebuild`. On a user's Mac
93405
- * with no build toolchain (and where the auto `prebuild-install` does not
93406
- * target the bundled Node's ABI/arch the way the DMG build's EXPLICIT
93407
- * `prebuild-install --runtime node --target <bundled> --arch <arch>` call does)
93408
- * that leaves the module SOURCE-ONLY → the closure has no `better-sqlite3`
93409
- * `.node` → the validator refuses the swap ("staged closure missing native
93410
- * prebuilds: better-sqlite3"). The docker image never hits this: its Node ABI
93411
- * has a published prebuild the auto path fetches cleanly, and it carries a
93412
- * build toolchain fallback.
93413
- *
93414
- * This mirrors the DMG build's explicit re-fetch (`scripts/stage-deps.mjs`)
93415
- * into the runtime staging path: for each traditional native that is present
93416
- * in the closure but missing its compiled `.node`, fetch the prebuild for the
93417
- * RUNNING Node ABI/arch (the same Node that will load the swapped closure).
93418
- *
93419
- * N-API natives (`sharp`, `node-av`) are ABI-stable and ship their binaries as
93420
- * platform SIBLING packages that npm fetches reliably with no compile step, so
93421
- * they are not in scope here — a genuine miss there is a broken install the
93422
- * validator should still reject, not something a prebuild re-fetch can heal.
93423
- *
93424
- * Injected into the update service from the construction site so the zero-dep
93425
- * `@camstack/node-root` layer never depends on npm/@camstack/system, and the
93426
- * fetch is unit-testable with a fake fetcher.
93427
- */
93428
- var execFileAsync = promisify(execFile);
93429
- /** Bounded recursive `.node` scan depth inside a module dir. */
93430
- var NATIVE_SCAN_DEPTH = 4;
93431
- /** Timeout for a single `prebuild-install` fetch. */
93432
- var PREBUILD_FETCH_TIMEOUT_MS = 12e4;
93433
- /**
93434
- * Traditional (compile-or-prebuild) natives an installed closure must carry a
93435
- * `.node` for. Only these are ENSURE-able by a prebuild re-fetch; N-API natives
93436
- * are excluded (see file header).
93437
- */
93438
- var TRADITIONAL_NATIVE_PACKAGES = ["better-sqlite3"];
93439
- /** Bounded recursive scan for ANY `*.node` file under `dir`. */
93440
- function hasDotNode(dir, maxDepth) {
93441
- let entries;
93442
- try {
93443
- entries = fs$17.readdirSync(dir, { withFileTypes: true });
93444
- } catch {
93445
- return false;
93446
- }
93447
- for (const entry of entries) if (entry.isFile() && entry.name.endsWith(".node")) return true;
93448
- if (maxDepth <= 0) return false;
93449
- for (const entry of entries) if (entry.isDirectory() && hasDotNode(path$39.join(dir, entry.name), maxDepth - 1)) return true;
93450
- return false;
93451
- }
93452
- /** Locate the bundled `prebuild-install` bin.js inside the staged closure. */
93453
- function resolvePrebuildInstallBin(closureDir, moduleDir) {
93454
- return [path$39.join(moduleDir, "node_modules", "prebuild-install", "bin.js"), path$39.join(closureDir, "node_modules", "prebuild-install", "bin.js")].find((c) => fs$17.existsSync(c)) ?? null;
93455
- }
93456
- /**
93457
- * Default fetcher: run the closure's own `prebuild-install` under the CURRENT
93458
- * Node (`process.execPath`) — no reliance on `npx`/`npm` on PATH (a packaged
93459
- * app ships a bare Node). Explicitly targets the running ABI/arch, mirroring
93460
- * `scripts/stage-deps.mjs`.
93461
- */
93462
- function makeDefaultFetch(closureDir, logger) {
93463
- return async (moduleDir, target) => {
93464
- const bin = resolvePrebuildInstallBin(closureDir, moduleDir);
93465
- if (bin === null) throw new Error(`prebuild-install not found in the staged closure for ${moduleDir}`);
93466
- logger.info("fetching native prebuild via prebuild-install", { meta: {
93467
- moduleDir,
93468
- bin,
93469
- nodeVersion: target.nodeVersion,
93470
- arch: target.arch
93471
- } });
93472
- await execFileAsync(process.execPath, [
93473
- bin,
93474
- "--runtime",
93475
- "node",
93476
- "--target",
93477
- target.nodeVersion,
93478
- "--arch",
93479
- target.arch
93480
- ], {
93481
- cwd: moduleDir,
93482
- timeout: PREBUILD_FETCH_TIMEOUT_MS
93483
- });
93484
- };
93485
- }
93486
- /**
93487
- * Ensure every traditional native in `<closureDir>/node_modules` carries its
93488
- * compiled `.node`, fetching the prebuild for the running ABI when missing.
93489
- *
93490
- * A NO-OP when nothing is missing (docker / tests) and when a native package is
93491
- * absent from the closure. Never throws for a single package's fetch failure —
93492
- * it logs and continues so the downstream `findMissingNativePrebuilds`
93493
- * validator makes the final swap/abort decision. Returns the packages whose
93494
- * prebuild was successfully fetched (for logging/tests).
93495
- */
93496
- async function ensureNativePrebuilds(closureDir, options) {
93497
- const nm = path$39.join(closureDir, "node_modules");
93498
- const target = options.target ?? {
93499
- nodeVersion: process.versions.node,
93500
- arch: process.arch
93501
- };
93502
- const packages = options.packages ?? TRADITIONAL_NATIVE_PACKAGES;
93503
- const fetchPrebuild = options.fetchPrebuild ?? makeDefaultFetch(closureDir, options.logger);
93504
- const fetched = [];
93505
- for (const pkg of packages) {
93506
- const moduleDir = path$39.join(nm, pkg);
93507
- if (!fs$17.existsSync(moduleDir)) continue;
93508
- if (hasDotNode(moduleDir, NATIVE_SCAN_DEPTH)) continue;
93509
- options.logger.warn("staged closure native prebuild missing — fetching for running ABI", { meta: {
93510
- pkg,
93511
- moduleDir,
93512
- nodeVersion: target.nodeVersion,
93513
- arch: target.arch
93514
- } });
93515
- try {
93516
- await fetchPrebuild(moduleDir, target);
93517
- } catch (err) {
93518
- options.logger.error("native prebuild fetch failed (validator will decide)", { meta: {
93519
- pkg,
93520
- error: errMsg(err)
93521
- } });
93522
- continue;
93523
- }
93524
- if (hasDotNode(moduleDir, NATIVE_SCAN_DEPTH)) {
93525
- fetched.push(pkg);
93526
- options.logger.info("native prebuild fetched", { meta: { pkg } });
93527
- } else options.logger.error("native prebuild still missing after fetch", { meta: { pkg } });
93528
- }
93529
- return fetched;
93530
- }
93531
- //#endregion
93532
- export { AGENT_CAP_FWD_ACTION, AGENT_CAP_FWD_SERVICE, AGENT_READINESS_SERVICE_NAME, ALL_CAPABILITY_DEFINITIONS, AddonApiFactory, AddonDepsManager, AddonEngineManager, AddonHealthMonitor, AddonInstaller, AddonLoader, AddonManifest, AddonRouteRegistry, AlertCenterAddon, ApiKeyManager, AuthManager, CLUSTER_SECRET_MISMATCH_TYPE, CLUSTER_SECRET_REJECTED_EXIT_CODE, CORE_CAP_SERVICE_NAME, CapRouteError, CapRouteResolver, CapUsageRegistry, CapabilityHandle, CapabilityRegistry, CapabilityUnavailableError, ConfigManager, ConfigStore, ConsoleDestination, ConsoleLoggingAddon, CustomActionRegistry, DEFAULT_DATA_PATH, DEVICE_SETTINGS_CONTRIBUTION_METHODS, DEVICE_STATUS_METHOD, DataPlaneRegistry, DeviceManagerAddon, DeviceRegistry, DeviceStore, EVENT_TOPIC_PREFIX, EngineManagerResolver, EventBus, FeatureManager, FilesystemStorageAddon, FilesystemStorageProvider, FrameDecoder, FsStorageBackend, HEALTH_MONITOR_GRACE_PERIOD_MS, HEALTH_MONITOR_RETRY_INTERVALS_MS, HEALTH_MONITOR_TICK_MS, HUB_CAP_FWD_ACTION, HUB_CAP_FWD_SERVICE, HubForwarderAddon, HubForwarderDestination, HubLogForwarder, HubNodeRegistry, INFRA_CAPABILITIES, IntegrationRegistry, JobJournal, LifecycleJobEngine, LifecycleStateMachine, LocalAuthAddon, LocalChildClient, LocalChildRegistry, LogManager, LogRingBuffer, METHOD_ACCESS_MAP, ModelDownloadService, NATIVE_PROVIDER_SERVICE_INFIX, NativeMetricsAddon, NativeMetricsProvider, NetworkQualityTracker, NotificationService, PYTHON_VERSION, PipelineRunner, PipelineValidator, PythonEnvManager, RESTART_MARKER_FILE, RUNTIME_DEFAULTS, ReadinessRegistry, ReadinessTimeoutError, ReplEngine, RingBuffer, ScopedLogger, ScopedTokenManager, SocketChannel, SqliteSettingsAddon, SqliteSettingsBackend, StagingArea, StorageLocationManager, StorageManager, StorageOrchestratorAddon, StorageOrchestratorService, SystemConfigAddon, SystemEventBus, TRADITIONAL_NATIVE_PACKAGES, ToastService, UDS_NO_ROUTE_PREFIX, UdsLocalTransportClient, UdsLocalTransportServer, UserManager, WinstonDestination, WinstonLoggingAddon, __resetCapUsageRegistryForTests, adaptBrokerToCluster, addonSettingsCapability, bootstrapSchema, brokerCallForCap, brokerTransportLink, buildBinaryPath, buildCapRouters, buildLinkChain, buildNativeCapProxy, buildNodeManifest, buildStorageLocationRegistry, buildUdsNativeCapProxy, builderMountedCapNames, callRegisterNodeWithRetry, callWithServiceDiscovery, capActionName, capActionSuffix, capBareAction, capServiceName, classifyCapRoute, clearPendingRestart, clusterEventTopic, clusterSecretMatches, collectModelFiles, contentTypeFor, copyDirRecursive, copyExtraFileDirs, createAddonContext, createAddonDataPlaneFacility, createAddonService, createAuthenticatedFileServer, createBroker, createBrokerDeviceManagerApi, createCoreCapService, createFileDataPlaneHandler, createHubCapForwardService, createHubService, createKernelHwAccel, createLocalTransport, createParentUnownedCallHandler, createProcessService, createReadinessService, createReadinessServiceForRegistry, createScopedProcessManager, createStreamProbeBrokerService, createUdsAddonContext, createUdsEventBridge, createUdsEventBus, createUdsLogger, createUdsLoggerWithControl, deleteModelFromDisk, deriveAgentListenPort, describeProviderKindDrift, detectWorkspacePackagesDir, downloadBinary, downloadFile, downloadModel, emitDownForOwnedCaps, encodeFrame, ensureBinary, ensureDir, ensureFfmpeg, ensureLibraryBuilt, ensureModel, ensureNativePrebuilds, ensurePython, ensureTlsCert, expandCapMethods, fetchJson, findInPath, formatLogLine, getBrokerEventBus, getCapUsageRegistry, getFfmpegDownloadUrl, getModelFilePath, getMoleculerEventStats, getOrInitReadinessRegistry, getOrInitReadinessRegistryForClient, getPidStats, getPlatformInfo, getPythonDownloadUrl, getRestartMarkerPath, getSinglePidStats, getWorkerDeviceRegistry, hashClusterSecret, installManifestNativeDeps, installManifestPythonDeps, installPackageFromNpm, installPythonPackages, installPythonRequirements, ipcChildLink, ipcParentLink, isAddonDeploySource, isArrayOutputSchema, isClusterSecretMismatchError, isCollectionArrayMethod, isInfraCapability, isModelDownloaded, isSourceNewer, loadTlsCert, localEndpointPath, localProviderLink, mountNativeCapService, parseCapAction, parseRangeHeader, parseTokenizedUrl, proxyToUpstream, readPendingRestart, readinessKey, registerEventBusService, resolveFilePath, resolveHwAccel, resolveNpmInvocation, runNpm, scheduleSelfRestart, scopeKey, scopesAllowDeviceCap, serializeTypedArrays, setHubConnected, setNodeEventInterest, stripCamstackDeps, subscribePassthrough, udsChildLogToWorkerEntry, validateProviderRegistrations, writePendingRestart };
93664
+ export { AGENT_CAP_FWD_ACTION, AGENT_CAP_FWD_SERVICE, AGENT_READINESS_SERVICE_NAME, ALL_CAPABILITY_DEFINITIONS, AddonApiFactory, AddonDepsManager, AddonEngineManager, AddonHealthMonitor, AddonInstaller, AddonLoader, AddonManifest, AddonRouteRegistry, AlertCenterAddon, ApiKeyManager, AuthManager, CLUSTER_SECRET_MISMATCH_TYPE, CLUSTER_SECRET_REJECTED_EXIT_CODE, CORE_CAP_SERVICE_NAME, CapRouteError, CapRouteResolver, CapUsageRegistry, CapabilityHandle, CapabilityRegistry, CapabilityUnavailableError, ConfigManager, ConfigStore, ConsoleDestination, ConsoleLoggingAddon, CustomActionRegistry, DEFAULT_DATA_PATH, DEVICE_SETTINGS_CONTRIBUTION_METHODS, DEVICE_STATUS_METHOD, DataPlaneRegistry, DeviceManagerAddon, DeviceRegistry, DeviceStore, EVENT_TOPIC_PREFIX, EngineManagerResolver, EventBus, FeatureManager, FilesystemStorageAddon, FilesystemStorageProvider, FrameDecoder, FsStorageBackend, HEALTH_MONITOR_GRACE_PERIOD_MS, HEALTH_MONITOR_RETRY_INTERVALS_MS, HEALTH_MONITOR_TICK_MS, HUB_CAP_FWD_ACTION, HUB_CAP_FWD_SERVICE, HubForwarderAddon, HubForwarderDestination, HubLogForwarder, HubNodeRegistry, INFRA_CAPABILITIES, IntegrationRegistry, JobJournal, LifecycleJobEngine, LifecycleStateMachine, LocalAuthAddon, LocalChildClient, LocalChildRegistry, LogManager, LogRingBuffer, METHOD_ACCESS_MAP, ModelDownloadService, NATIVE_PROVIDER_SERVICE_INFIX, NATIVE_SCAN_DEPTH, NativeMetricsAddon, NativeMetricsProvider, NetworkQualityTracker, NotificationService, PYTHON_VERSION, PipelineRunner, PipelineValidator, PythonEnvManager, RESTART_MARKER_FILE, RUNTIME_DEFAULTS, ReadinessRegistry, ReadinessTimeoutError, ReplEngine, RingBuffer, ScopedLogger, ScopedTokenManager, SocketChannel, SqliteSettingsAddon, SqliteSettingsBackend, StagingArea, StorageLocationManager, StorageManager, StorageOrchestratorAddon, StorageOrchestratorService, SystemConfigAddon, SystemEventBus, TRADITIONAL_NATIVE_PACKAGES, ToastService, UDS_NO_ROUTE_PREFIX, UdsLocalTransportClient, UdsLocalTransportServer, UserManager, WinstonDestination, WinstonLoggingAddon, __resetCapUsageRegistryForTests, adaptBrokerToCluster, addonSettingsCapability, bootstrapSchema, brokerCallForCap, brokerTransportLink, buildBinaryPath, buildCapRouters, buildLinkChain, buildNativeCapProxy, buildNodeManifest, buildStorageLocationRegistry, buildUdsNativeCapProxy, builderMountedCapNames, callRegisterNodeWithRetry, callWithServiceDiscovery, capActionName, capActionSuffix, capBareAction, capServiceName, classifyCapRoute, clearPendingRestart, clusterEventTopic, clusterSecretMatches, collectModelFiles, contentTypeFor, copyDirRecursive, copyExtraFileDirs, createAddonContext, createAddonDataPlaneFacility, createAddonService, createAuthenticatedFileServer, createBroker, createBrokerDeviceManagerApi, createCoreCapService, createFileDataPlaneHandler, createHubCapForwardService, createHubService, createKernelHwAccel, createLocalTransport, createParentUnownedCallHandler, createProcessService, createReadinessService, createReadinessServiceForRegistry, createScopedProcessManager, createStreamProbeBrokerService, createUdsAddonContext, createUdsEventBridge, createUdsEventBus, createUdsLogger, createUdsLoggerWithControl, deleteModelFromDisk, deriveAgentListenPort, describeProviderKindDrift, detectWorkspacePackagesDir, downloadBinary, downloadFile, downloadModel, emitDownForOwnedCaps, encodeFrame, ensureAddonNativePrebuilds, ensureBinary, ensureDir, ensureFfmpeg, ensureLibraryBuilt, ensureModel, ensureNativePrebuilds, ensurePython, ensureTlsCert, expandCapMethods, fetchJson, findInPath, formatLogLine, getBrokerEventBus, getCapUsageRegistry, getFfmpegDownloadUrl, getModelFilePath, getMoleculerEventStats, getOrInitReadinessRegistry, getOrInitReadinessRegistryForClient, getPidStats, getPlatformInfo, getPythonDownloadUrl, getRestartMarkerPath, getSinglePidStats, getWorkerDeviceRegistry, hasDotNode, hashClusterSecret, installManifestNativeDeps, installManifestPythonDeps, installPackageFromNpm, installPythonPackages, installPythonRequirements, ipcChildLink, ipcParentLink, isAddonDeploySource, isArrayOutputSchema, isClusterSecretMismatchError, isCollectionArrayMethod, isInfraCapability, isModelDownloaded, isSourceNewer, loadTlsCert, localEndpointPath, localProviderLink, mountNativeCapService, parseCapAction, parseRangeHeader, parseTokenizedUrl, proxyToUpstream, readPendingRestart, readinessKey, registerEventBusService, resolveFilePath, resolveHwAccel, resolveNpmInvocation, runNpm, scheduleSelfRestart, scopeKey, scopesAllowDeviceCap, serializeTypedArrays, setHubConnected, setNodeEventInterest, stripCamstackDeps, subscribePassthrough, udsChildLogToWorkerEntry, validateProviderRegistrations, writePendingRestart };
@@ -0,0 +1,41 @@
1
+ import { IScopedLogger } from '@camstack/types';
2
+ import { PrebuildFetchFn, PrebuildTarget } from './ensure-native-prebuilds.js';
3
+ /** Per-native outcome of the ensure pass. */
4
+ export type NativeEnsureOutcome = 'already-present' | 'copied' | 'fetched' | 'missing';
5
+ export interface NativeEnsureResult {
6
+ readonly pkg: string;
7
+ readonly outcome: NativeEnsureOutcome;
8
+ }
9
+ export interface EnsureAddonNativePrebuildsOptions {
10
+ /** Addon package name — for loud error messages naming module + addon. */
11
+ readonly addonName: string;
12
+ readonly logger: IScopedLogger;
13
+ /**
14
+ * Bundled addon copy dir (the `installCopy` `sourceDir` / a
15
+ * `CAMSTACK_BUNDLED_ADDONS_DIR/<addon>` copy) whose own
16
+ * `node_modules/<pkg>` may carry a prebuilt `.node` for THIS machine.
17
+ * Copied in directly before any network fetch. Absent on the npm/tgz path.
18
+ */
19
+ readonly bundledSourceDir?: string;
20
+ /** Running ABI/arch the fetched prebuild must match. Defaults to the process. */
21
+ readonly target?: PrebuildTarget;
22
+ /** Native package set. Defaults to `TRADITIONAL_NATIVE_PACKAGES`. */
23
+ readonly packages?: readonly string[];
24
+ /** Override the fetcher (tests). Production uses the bundled `prebuild-install`. */
25
+ readonly fetchPrebuild?: PrebuildFetchFn;
26
+ /** Override the `.node` presence scan (tests). */
27
+ readonly hasNative?: (moduleDir: string) => boolean;
28
+ }
29
+ /**
30
+ * Ensure every referenced traditional native in `<addonDir>/node_modules`
31
+ * carries its compiled `.node` after the addon's `npm install`. Bundled-copy
32
+ * first (offline), prebuild-fetch fallback, LOUD throw on a residual miss.
33
+ *
34
+ * SKIPS a native absent from the addon's own `node_modules` (it resolves via a
35
+ * hoisted host copy — not this addon's concern). A NO-OP in docker / dev where
36
+ * npm produced a working `.node` (already-present). Returns the per-native
37
+ * outcomes for logging/tests.
38
+ *
39
+ * @throws if any required native is still source-only after copy + fetch.
40
+ */
41
+ export declare function ensureAddonNativePrebuilds(addonDir: string, options: EnsureAddonNativePrebuildsOptions): Promise<readonly NativeEnsureResult[]>;
@@ -1,4 +1,6 @@
1
1
  import { IScopedLogger } from '@camstack/types';
2
+ /** Bounded recursive `.node` scan depth inside a module dir. */
3
+ export declare const NATIVE_SCAN_DEPTH = 4;
2
4
  /**
3
5
  * Traditional (compile-or-prebuild) natives an installed closure must carry a
4
6
  * `.node` for. Only these are ENSURE-able by a prebuild re-fetch; N-API natives
@@ -23,6 +25,8 @@ export interface EnsureNativePrebuildsOptions {
23
25
  /** Override the fetcher (tests). Production uses the bundled `prebuild-install`. */
24
26
  readonly fetchPrebuild?: PrebuildFetchFn;
25
27
  }
28
+ /** Bounded recursive scan for ANY `*.node` file under `dir`. */
29
+ export declare function hasDotNode(dir: string, maxDepth: number): boolean;
26
30
  /**
27
31
  * Ensure every traditional native in `<closureDir>/node_modules` carries its
28
32
  * compiled `.node`, fetching the prebuild for the running ABI when missing.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/system",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
4
4
  "description": "Core addon for CamStack — builtins, pipeline, process management, auth, logging, events",
5
5
  "keywords": [
6
6
  "camstack",