@camstack/system 1.2.179 → 1.2.181

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/addon-runner.js +2 -2
  2. package/dist/addon-runner.mjs +2 -2
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.d.ts +56 -10
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +657 -62
  11. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +658 -63
  12. package/dist/builtins/backup-orchestrator/run-queue.d.ts +103 -0
  13. package/dist/builtins/backup-orchestrator/staging-paths.d.ts +36 -0
  14. package/dist/builtins/console-logging/index.js +1 -1
  15. package/dist/builtins/console-logging/index.mjs +1 -1
  16. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  17. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  18. package/dist/builtins/device-manager/device-manager.addon.js +349 -65
  19. package/dist/builtins/device-manager/device-manager.addon.mjs +349 -65
  20. package/dist/builtins/device-manager/device-meta-actions.d.ts +8 -19
  21. package/dist/builtins/device-manager/device-provider-context.d.ts +4 -0
  22. package/dist/builtins/device-manager/migrate-device.d.ts +47 -3
  23. package/dist/builtins/device-manager/migrate-guard.d.ts +39 -0
  24. package/dist/builtins/device-manager/migrate-hardware-state.d.ts +24 -0
  25. package/dist/builtins/device-manager/migrate-reload.d.ts +8 -0
  26. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  27. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  28. package/dist/builtins/hub-forwarder/index.js +1 -1
  29. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  30. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  31. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  32. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  33. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  34. package/dist/builtins/local-network/local-network.addon.js +1 -1
  35. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  36. package/dist/builtins/loki-logging/index.js +1 -1
  37. package/dist/builtins/loki-logging/index.mjs +1 -1
  38. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  39. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  40. package/dist/builtins/platform-probe/index.js +1 -1
  41. package/dist/builtins/platform-probe/index.mjs +1 -1
  42. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  43. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  44. package/dist/builtins/snapshot/index.js +1 -1
  45. package/dist/builtins/snapshot/index.mjs +1 -1
  46. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  47. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  48. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  49. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  50. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  51. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  52. package/dist/builtins/system-backup/system-backup.service.d.ts +55 -2
  53. package/dist/builtins/system-config/system-config.addon.js +1 -1
  54. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  55. package/dist/builtins/winston-logging/index.js +1 -1
  56. package/dist/builtins/winston-logging/index.mjs +1 -1
  57. package/dist/{dist-L_39vRmH.mjs → dist-DbnNk4Lw.mjs} +211 -2
  58. package/dist/{dist-C7qWwrK1.js → dist-whe0b4_3.js} +211 -2
  59. package/dist/index.js +530 -2
  60. package/dist/index.mjs +533 -5
  61. package/dist/kernel/addon-installer.d.ts +69 -0
  62. package/dist/kernel/deps/closure-dedupe.d.ts +105 -0
  63. package/dist/{manifest-system-deps-CMOGc5nC.mjs → manifest-system-deps-DOcMuUWK.mjs} +1 -1
  64. package/dist/{manifest-system-deps-PAIfmNun.js → manifest-system-deps-y5vZBO6q.js} +1 -1
  65. package/dist/{retired-settings-keys-3soU9pfP.js → retired-settings-keys-BdvmiaK7.js} +1 -1
  66. package/dist/{retired-settings-keys-CnbKAxmX.mjs → retired-settings-keys-CyQG8b-A.mjs} +1 -1
  67. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("./chunk-Cek0wNdY.js");
3
- const require_dist = require("./dist-C7qWwrK1.js");
3
+ const require_dist = require("./dist-whe0b4_3.js");
4
4
  const require_builtins_alerts_alerts_addon = require("./builtins/alerts/alerts.addon.js");
5
5
  require("./builtins/alerts/index.js");
6
6
  const require_formatter = require("./formatter-DqAKDlvN.js");
@@ -27,7 +27,7 @@ require("./builtins/system-config/index.js");
27
27
  const require_builtins_winston_logging_index = require("./builtins/winston-logging/index.js");
28
28
  const require_file_data_plane = require("./file-data-plane-DO8KbxCe.js");
29
29
  const require_tls$1 = require("./tls-BxQlomxd.js");
30
- const require_manifest_system_deps = require("./manifest-system-deps-PAIfmNun.js");
30
+ const require_manifest_system_deps = require("./manifest-system-deps-y5vZBO6q.js");
31
31
  const require_resource_monitor = require("./resource-monitor-CdnzxBLP.js");
32
32
  const require_custom_action_registry = require("./custom-action-registry-jY0NOZK8.js");
33
33
  let zod = require("zod");
@@ -3678,8 +3678,362 @@ var AddonManifest = class {
3678
3678
  }
3679
3679
  };
3680
3680
  //#endregion
3681
+ //#region src/kernel/deps/closure-dedupe.ts
3682
+ /**
3683
+ * Closure dedupe — stop copying into every addon the libraries the server
3684
+ * closure already provides.
3685
+ *
3686
+ * `installRuntimeDependencies` materialises each addon's declared
3687
+ * `camstack.runtimeDependencies` (and their whole transitive tree) under
3688
+ * `<addonsDir>/<pkg>/node_modules`. Measured on the live hub (2026-09-04):
3689
+ * 196 MB across 23 addons, largely re-copies of libraries sitting one
3690
+ * directory away in `/data/server-root/current/node_modules` — `mediabunny`
3691
+ * (11 MB) twice, `werift` (~5 MB) three times, plus their shared subtrees
3692
+ * (`@noble`, `@peculiar`, `werift-*`).
3693
+ *
3694
+ * The fix: after an addon tree's npm install completes, every top-level
3695
+ * `node_modules` entry the closure ALSO provides at a compatible version is
3696
+ * replaced by a SYMLINK into the closure's `node_modules`. Symlinks preserve
3697
+ * standard Node resolution for BOTH module systems at ANY depth (verified on
3698
+ * the hub: `require()` and `import()` through a symlink on `/data/addons`
3699
+ * both resolve), and a dependency the closure later drops fails LOUDLY
3700
+ * (dangling link → `Cannot find module`) instead of silently degrading.
3701
+ *
3702
+ * Version compatibility is the sharp edge, measured live: the closure ships
3703
+ * `zod@4.5.4` while `@apocaliss92/nodelink-js` (provider-reolink) requires
3704
+ * `zod@^3` and carries `3.25.76`. A link there would silently downgrade a
3705
+ * dependent to an incompatible major — worse than the disk it saves. So a
3706
+ * link happens ONLY when the closure version satisfies EVERY requirement
3707
+ * range recorded for that package in the addon's own tree, under a
3708
+ * deliberately conservative range parser (exact / caret / tilde; anything
3709
+ * else refuses). Identical versions always link.
3710
+ *
3711
+ * Everything here is dependency-free on purpose (no `semver` package in the
3712
+ * kernel): the parser handles only the forms it can prove, and refusal keeps
3713
+ * the addon's own copy — never a wrong resolution.
3714
+ */
3715
+ function parseVersion(value) {
3716
+ const m = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(value.trim());
3717
+ if (!m) return null;
3718
+ return {
3719
+ major: Number(m[1]),
3720
+ minor: Number(m[2]),
3721
+ patch: Number(m[3]),
3722
+ prerelease: m[4] ?? null
3723
+ };
3724
+ }
3725
+ /** -1 / 0 / 1 over the numeric triple only (callers gate prereleases first). */
3726
+ function compareTriple(a, b) {
3727
+ if (a.major !== b.major) return a.major < b.major ? -1 : 1;
3728
+ if (a.minor !== b.minor) return a.minor < b.minor ? -1 : 1;
3729
+ if (a.patch !== b.patch) return a.patch < b.patch ? -1 : 1;
3730
+ return 0;
3731
+ }
3732
+ /**
3733
+ * Does `version` satisfy `range`, for the range forms this module is willing
3734
+ * to vouch for?
3735
+ *
3736
+ * Supported: exact (`1.2.3`, `=1.2.3`), caret (`^1.2.3`, incl. the 0.x rules)
3737
+ * and tilde (`~1.2.3`). EVERYTHING else — `*`, `>=`, unions, hyphen ranges,
3738
+ * dist-tags, bases with prereleases — returns false: an unprovable range must
3739
+ * keep the addon's own copy, never borrow the closure's.
3740
+ *
3741
+ * A prerelease `version` satisfies only an exact string match — semver range
3742
+ * semantics around prereleases are precisely the kind of subtlety a
3743
+ * hand-rolled parser must refuse rather than approximate.
3744
+ */
3745
+ function closureSatisfiesRange(range, version) {
3746
+ const trimmedRange = range.trim();
3747
+ const v = parseVersion(version);
3748
+ if (v === null) return false;
3749
+ if (v.prerelease !== null) return trimmedRange === version.trim() || trimmedRange === `=${version.trim()}`;
3750
+ const exactBase = parseVersion(trimmedRange.startsWith("=") ? trimmedRange.slice(1) : trimmedRange);
3751
+ if (exactBase !== null) return exactBase.prerelease === null && compareTriple(v, exactBase) === 0;
3752
+ const operator = trimmedRange[0];
3753
+ if (operator !== "^" && operator !== "~") return false;
3754
+ const base = parseVersion(trimmedRange.slice(1));
3755
+ if (base === null || base.prerelease !== null) return false;
3756
+ if (operator === "~") return v.major === base.major && v.minor === base.minor && compareTriple(v, base) >= 0;
3757
+ if (base.major > 0) return v.major === base.major && compareTriple(v, base) >= 0;
3758
+ if (base.minor > 0) return v.major === 0 && v.minor === base.minor && compareTriple(v, base) >= 0;
3759
+ return compareTriple(v, base) === 0;
3760
+ }
3761
+ /**
3762
+ * Belt over the range check: the closure copy must live in the same major
3763
+ * line as the copy npm actually materialised (same minor while major is 0).
3764
+ * The range check should already guarantee this; if the two ever disagree
3765
+ * the safe answer is "do not link".
3766
+ */
3767
+ function sameCompatLine(a, b) {
3768
+ const pa = parseVersion(a);
3769
+ const pb = parseVersion(b);
3770
+ if (pa === null || pb === null) return false;
3771
+ if (pa.major !== pb.major) return false;
3772
+ if (pa.major === 0) return pa.minor === pb.minor;
3773
+ return true;
3774
+ }
3775
+ /**
3776
+ * Decide, per top-level entry, whether the closure's copy can serve in place
3777
+ * of the addon's own. Identical versions always link; otherwise every
3778
+ * recorded requirement range must accept the closure version, and the two
3779
+ * copies must share a compat line. Anything unprovable keeps the local copy.
3780
+ */
3781
+ function planClosureDedupe(input) {
3782
+ const links = [];
3783
+ const keeps = [];
3784
+ for (const candidate of input.candidates) {
3785
+ if (candidate.name.startsWith("@camstack/")) {
3786
+ keeps.push({
3787
+ name: candidate.name,
3788
+ reason: "camstack-package"
3789
+ });
3790
+ continue;
3791
+ }
3792
+ if (input.neverLink.has(candidate.name)) {
3793
+ keeps.push({
3794
+ name: candidate.name,
3795
+ reason: "never-link"
3796
+ });
3797
+ continue;
3798
+ }
3799
+ const closureVersion = input.closureVersions.get(candidate.name);
3800
+ if (closureVersion === void 0) {
3801
+ keeps.push({
3802
+ name: candidate.name,
3803
+ reason: "not-in-closure"
3804
+ });
3805
+ continue;
3806
+ }
3807
+ if (closureVersion === candidate.version) {
3808
+ links.push({
3809
+ name: candidate.name,
3810
+ closureVersion
3811
+ });
3812
+ continue;
3813
+ }
3814
+ const ranges = input.requirements.get(candidate.name) ?? [];
3815
+ if (ranges.length > 0 && ranges.every((range) => closureSatisfiesRange(range, closureVersion)) && sameCompatLine(closureVersion, candidate.version)) links.push({
3816
+ name: candidate.name,
3817
+ closureVersion
3818
+ });
3819
+ else keeps.push({
3820
+ name: candidate.name,
3821
+ reason: "version-incompatible"
3822
+ });
3823
+ }
3824
+ return {
3825
+ links,
3826
+ keeps
3827
+ };
3828
+ }
3829
+ function readTreeManifest(pkgJsonPath) {
3830
+ let raw;
3831
+ try {
3832
+ raw = require_dist.asJsonObject(require_dist.parseJsonUnknown(node_fs.readFileSync(pkgJsonPath, "utf-8")));
3833
+ } catch {
3834
+ return null;
3835
+ }
3836
+ if (raw === null) return null;
3837
+ const ranges = /* @__PURE__ */ new Map();
3838
+ for (const key of [
3839
+ "dependencies",
3840
+ "optionalDependencies",
3841
+ "peerDependencies"
3842
+ ]) {
3843
+ const deps = require_dist.asJsonObject(raw[key]);
3844
+ if (deps === null) continue;
3845
+ for (const [name, range] of Object.entries(deps)) if (typeof range === "string" && !ranges.has(name)) ranges.set(name, range);
3846
+ }
3847
+ const version = require_dist.asString(raw["version"]);
3848
+ return {
3849
+ version: version.length > 0 ? version : null,
3850
+ ranges
3851
+ };
3852
+ }
3853
+ /** Top-level entries of a node_modules dir (scoped names flattened). */
3854
+ function listTopLevelEntries(nodeModulesDir) {
3855
+ let entries;
3856
+ try {
3857
+ entries = node_fs.readdirSync(nodeModulesDir, { withFileTypes: true });
3858
+ } catch {
3859
+ return [];
3860
+ }
3861
+ const names = [];
3862
+ for (const entry of entries) {
3863
+ if (entry.name.startsWith(".")) continue;
3864
+ if (entry.name.startsWith("@")) {
3865
+ let inner;
3866
+ try {
3867
+ inner = node_fs.readdirSync(node_path.join(nodeModulesDir, entry.name), { withFileTypes: true });
3868
+ } catch {
3869
+ continue;
3870
+ }
3871
+ for (const sub of inner) if (!sub.name.startsWith(".")) names.push(`${entry.name}/${sub.name}`);
3872
+ } else names.push(entry.name);
3873
+ }
3874
+ return names;
3875
+ }
3876
+ /**
3877
+ * Read an addon tree's dedupe-relevant state: which top-level packages exist
3878
+ * (and their versions), and which requirement ranges apply to each. A
3879
+ * dependent's range for `X` is recorded only when that dependent resolves to
3880
+ * the TOP-LEVEL `X` — a nested override copy (`dep/node_modules/X`) means the
3881
+ * flat copy is not what that dependent loads, so its range must not block the
3882
+ * flat copy's dedupe.
3883
+ */
3884
+ function collectAddonTreeState(addonDir) {
3885
+ const nodeModulesDir = node_path.join(addonDir, "node_modules");
3886
+ const requirements = /* @__PURE__ */ new Map();
3887
+ const addRange = (name, range) => {
3888
+ const existing = requirements.get(name);
3889
+ if (existing === void 0) requirements.set(name, [range]);
3890
+ else existing.push(range);
3891
+ };
3892
+ const rootManifest = readTreeManifest(node_path.join(addonDir, "package.json"));
3893
+ if (rootManifest !== null) for (const [name, range] of rootManifest.ranges) addRange(name, range);
3894
+ const candidates = [];
3895
+ const existingLinks = [];
3896
+ for (const name of listTopLevelEntries(nodeModulesDir)) {
3897
+ const entryDir = node_path.join(nodeModulesDir, name);
3898
+ let isLink = false;
3899
+ try {
3900
+ isLink = node_fs.lstatSync(entryDir).isSymbolicLink();
3901
+ } catch {
3902
+ continue;
3903
+ }
3904
+ if (isLink) {
3905
+ let target = "";
3906
+ try {
3907
+ target = node_fs.readlinkSync(entryDir);
3908
+ } catch {}
3909
+ existingLinks.push({
3910
+ name,
3911
+ target
3912
+ });
3913
+ }
3914
+ const manifest = readTreeManifest(node_path.join(entryDir, "package.json"));
3915
+ if (manifest === null) continue;
3916
+ if (!isLink && manifest.version !== null) candidates.push({
3917
+ name,
3918
+ version: manifest.version
3919
+ });
3920
+ for (const [depName, range] of manifest.ranges) {
3921
+ if (node_fs.existsSync(node_path.join(entryDir, "node_modules", depName))) continue;
3922
+ addRange(depName, range);
3923
+ }
3924
+ }
3925
+ return {
3926
+ candidates,
3927
+ requirements,
3928
+ existingLinks
3929
+ };
3930
+ }
3931
+ /** Read `name → version` from a closure node_modules for the given names. */
3932
+ function readClosureVersions(closureNodeModulesDir, names) {
3933
+ const versions = /* @__PURE__ */ new Map();
3934
+ for (const name of names) {
3935
+ const manifest = readTreeManifest(node_path.join(closureNodeModulesDir, name, "package.json"));
3936
+ if (manifest !== null && manifest.version !== null) versions.set(name, manifest.version);
3937
+ }
3938
+ return versions;
3939
+ }
3940
+ /**
3941
+ * Where the running node's server closure keeps its node_modules, or null
3942
+ * when there is none to dedupe against (dev workspace, tests without a
3943
+ * closure). `CAMSTACK_FRAMEWORK_DIR` is set by the starter/launcher on every
3944
+ * deployed node — the same directory the framework resolver hook and
3945
+ * NODE_PATH extension already point at.
3946
+ */
3947
+ function resolveClosureNodeModulesDir(explicitDir) {
3948
+ const frameworkDir = explicitDir ?? process.env["CAMSTACK_FRAMEWORK_DIR"];
3949
+ if (frameworkDir === void 0 || frameworkDir.length === 0) return null;
3950
+ const nodeModulesDir = explicitDir ?? node_path.join(frameworkDir, "node_modules");
3951
+ return node_fs.existsSync(nodeModulesDir) ? nodeModulesDir : null;
3952
+ }
3953
+ function symlinkType() {
3954
+ return process.platform === "win32" ? "junction" : "dir";
3955
+ }
3956
+ /**
3957
+ * Replace each planned entry with a symlink into the closure. Probes symlink
3958
+ * support once (Windows without developer mode cannot create them — in that
3959
+ * case every copy is kept, which is exactly today's behaviour). Each link
3960
+ * re-verifies the closure version right before the swap so a closure updated
3961
+ * mid-flight can only cause a KEEP, never a wrong link.
3962
+ *
3963
+ * Returns the names actually linked.
3964
+ */
3965
+ function executeClosureDedupe(addonDir, closureNodeModulesDir, plan, logger) {
3966
+ if (plan.links.length === 0) return [];
3967
+ const nodeModulesDir = node_path.join(addonDir, "node_modules");
3968
+ const probePath = node_path.join(nodeModulesDir, ".closure-link-probe");
3969
+ try {
3970
+ node_fs.mkdirSync(nodeModulesDir, { recursive: true });
3971
+ node_fs.rmSync(probePath, { force: true });
3972
+ node_fs.symlinkSync(closureNodeModulesDir, probePath, symlinkType());
3973
+ node_fs.rmSync(probePath, { force: true });
3974
+ } catch (err) {
3975
+ logger.warn("closure dedupe skipped — symlinks unavailable on this volume", { meta: {
3976
+ addonDir,
3977
+ error: require_dist.errMsg(err)
3978
+ } });
3979
+ return [];
3980
+ }
3981
+ const linked = [];
3982
+ for (const link of plan.links) {
3983
+ const target = node_path.join(closureNodeModulesDir, link.name);
3984
+ const manifest = readTreeManifest(node_path.join(target, "package.json"));
3985
+ if (manifest === null || manifest.version !== link.closureVersion) {
3986
+ logger.warn("closure dedupe — closure changed under the plan; keeping own copy", { meta: {
3987
+ name: link.name,
3988
+ planned: link.closureVersion,
3989
+ found: manifest?.version ?? null
3990
+ } });
3991
+ continue;
3992
+ }
3993
+ const entryDir = node_path.join(nodeModulesDir, link.name);
3994
+ try {
3995
+ node_fs.rmSync(entryDir, {
3996
+ recursive: true,
3997
+ force: true
3998
+ });
3999
+ node_fs.mkdirSync(node_path.dirname(entryDir), { recursive: true });
4000
+ node_fs.symlinkSync(target, entryDir, symlinkType());
4001
+ linked.push(link.name);
4002
+ } catch (err) {
4003
+ logger.warn("closure dedupe — link failed; entry removed copy may need reinstall", { meta: {
4004
+ name: link.name,
4005
+ error: require_dist.errMsg(err)
4006
+ } });
4007
+ }
4008
+ }
4009
+ return linked;
4010
+ }
4011
+ /**
4012
+ * Is `linkTarget` (as read from `readlink`) a path inside the closure's
4013
+ * node_modules? Only such links belong to this mechanism — workspace/npm
4014
+ * `file:` links must never be touched by dedupe maintenance.
4015
+ */
4016
+ function isClosureLinkTarget(linkTarget, closureNodeModulesDir) {
4017
+ if (linkTarget.length === 0) return false;
4018
+ const normalized = node_path.resolve(linkTarget);
4019
+ const root = node_path.resolve(closureNodeModulesDir);
4020
+ return normalized === root || normalized.startsWith(`${root}${node_path.sep}`);
4021
+ }
4022
+ //#endregion
3681
4023
  //#region src/kernel/addon-installer.ts
3682
4024
  var execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
4025
+ /**
4026
+ * Names declared in `camstack.nativeDependencies` — per-addon natives that
4027
+ * the closure dedupe must never replace with a link (their install path owns
4028
+ * ABI/prebuild handling; see `deps/manifest-native-deps.ts`).
4029
+ */
4030
+ function readNativeDependencyNames(pkg) {
4031
+ if (pkg === void 0) return /* @__PURE__ */ new Set();
4032
+ const camstack = require_dist.asJsonObject(pkg["camstack"]);
4033
+ if (camstack === null) return /* @__PURE__ */ new Set();
4034
+ const native = require_dist.asJsonObject(camstack["nativeDependencies"]);
4035
+ return native === null ? /* @__PURE__ */ new Set() : new Set(Object.keys(native));
4036
+ }
3683
4037
  function parseInstallSource(value) {
3684
4038
  switch (value) {
3685
4039
  case "npm":
@@ -3776,6 +4130,8 @@ var AddonInstaller = class AddonInstaller {
3776
4130
  installSource;
3777
4131
  logger;
3778
4132
  isPackageResolvable;
4133
+ /** Test seam — production resolves via `CAMSTACK_FRAMEWORK_DIR`. */
4134
+ closureNodeModulesDir;
3779
4135
  /**
3780
4136
  * Central manifest of installed addons. Tracks version + source +
3781
4137
  * timestamps + last-backup pointer per package, written atomically
@@ -3797,6 +4153,7 @@ var AddonInstaller = class AddonInstaller {
3797
4153
  this.logger = logger;
3798
4154
  this.manifest = new AddonManifest(config.addonsDir);
3799
4155
  this.isPackageResolvable = config.isPackageResolvable;
4156
+ this.closureNodeModulesDir = config.closureNodeModulesDir;
3800
4157
  if (config.installSource) this.installSource = config.installSource;
3801
4158
  else this.installSource = "npm";
3802
4159
  }
@@ -3984,6 +4341,140 @@ var AddonInstaller = class AddonInstaller {
3984
4341
  return changes;
3985
4342
  }
3986
4343
  /**
4344
+ * Make every INSTALLED addon's runtime dependencies resolvable — doing the
4345
+ * least work that achieves it. Runs at boot on hub and agent, which is what
4346
+ * turns a restore back into a working node: a system backup archives no
4347
+ * node_modules (D338), so before this pass every restored non-bootstrap
4348
+ * addon failed its first import until someone redeployed it by hand.
4349
+ *
4350
+ * Per addon, in order:
4351
+ * 1. drop closure links that no longer hold (dangling, or the closure
4352
+ * moved to a version the addon tree's recorded ranges refuse) — those
4353
+ * addons get a reinstall;
4354
+ * 2. a declared dep that already resolves in the addon tree costs nothing;
4355
+ * 3. a missing declared dep the closure provides at a COMPATIBLE version
4356
+ * becomes a link — no npm, no network;
4357
+ * 4. only an addon left with a genuinely unresolvable dep gets an npm
4358
+ * install (then dedupe + native ensure, same tail as a fresh install).
4359
+ *
4360
+ * "Only when necessary" is the contract: an addon whose deps are all
4361
+ * satisfied is not touched at all. Failures are per-addon and non-fatal —
4362
+ * a dep that cannot be re-fetched leaves that addon exactly as broken as
4363
+ * it already was, but LOUDLY, and never blocks the rest of the boot.
4364
+ */
4365
+ async ensureAddonRuntimeDeps() {
4366
+ const closureNodeModules = resolveClosureNodeModulesDir(this.closureNodeModulesDir);
4367
+ const linked = [];
4368
+ const reinstalled = [];
4369
+ const failed = [];
4370
+ const installedPackages = this.listInstalled();
4371
+ for (const pkg of installedPackages) {
4372
+ try {
4373
+ if (node_fs.lstatSync(pkg.dir).isSymbolicLink()) continue;
4374
+ } catch {
4375
+ continue;
4376
+ }
4377
+ try {
4378
+ const outcome = this.ensureOneAddonDeps(pkg.dir, pkg.name, closureNodeModules);
4379
+ for (const dep of outcome.linked) linked.push(`${pkg.name}:${dep}`);
4380
+ if (outcome.needsInstall) {
4381
+ await this.reinstallAddonDeps(pkg.dir, pkg.name);
4382
+ reinstalled.push(pkg.name);
4383
+ }
4384
+ } catch (err) {
4385
+ failed.push(pkg.name);
4386
+ this.logger.error(`${pkg.name} — runtime dependencies could NOT be ensured; the addon will fail to load until its deps install`, { meta: {
4387
+ dir: pkg.dir,
4388
+ error: require_dist.errMsg(err)
4389
+ } });
4390
+ }
4391
+ }
4392
+ return {
4393
+ checked: installedPackages.length,
4394
+ linked,
4395
+ reinstalled,
4396
+ failed
4397
+ };
4398
+ }
4399
+ /**
4400
+ * One addon's dependency audit (steps 1–3 of `ensureAddonRuntimeDeps`).
4401
+ * Pure decision + link creation; the npm fallback is the caller's.
4402
+ */
4403
+ ensureOneAddonDeps(addonDir, packageName, closureNodeModules) {
4404
+ const pkgView = readPackageJson(node_path.join(addonDir, "package.json"));
4405
+ const declared = pkgView === null ? null : require_dist.asJsonObject(pkgView.raw["dependencies"]);
4406
+ const declaredEntries = declared === null ? [] : Object.entries(declared);
4407
+ const state = collectAddonTreeState(addonDir);
4408
+ const nodeModulesDir = node_path.join(addonDir, "node_modules");
4409
+ let needsInstall = false;
4410
+ if (closureNodeModules !== null) for (const link of state.existingLinks) {
4411
+ if (!isClosureLinkTarget(link.target, closureNodeModules)) continue;
4412
+ const linkDir = node_path.join(nodeModulesDir, link.name);
4413
+ const targetManifest = node_fs.existsSync(node_path.join(linkDir, "package.json"));
4414
+ const ranges = state.requirements.get(link.name) ?? [];
4415
+ let holds = targetManifest;
4416
+ if (holds && ranges.length > 0) {
4417
+ const version = readClosureVersions(closureNodeModules, [link.name]).get(link.name);
4418
+ holds = version !== void 0 && ranges.every((range) => closureSatisfiesRange(range, version));
4419
+ }
4420
+ if (!holds) {
4421
+ this.logger.warn(`${packageName} — closure link for ${link.name} no longer holds; reinstalling this addon's deps`, { meta: { target: link.target } });
4422
+ node_fs.rmSync(linkDir, { force: true });
4423
+ needsInstall = true;
4424
+ }
4425
+ }
4426
+ const linked = [];
4427
+ for (const [depName, range] of declaredEntries) {
4428
+ if (typeof range !== "string") continue;
4429
+ if (node_fs.existsSync(node_path.join(nodeModulesDir, depName, "package.json"))) continue;
4430
+ const closureVersion = closureNodeModules === null ? void 0 : readClosureVersions(closureNodeModules, [depName]).get(depName);
4431
+ if (closureNodeModules !== null && closureVersion !== void 0 && closureSatisfiesRange(range, closureVersion)) {
4432
+ if (executeClosureDedupe(addonDir, closureNodeModules, {
4433
+ links: [{
4434
+ name: depName,
4435
+ closureVersion
4436
+ }],
4437
+ keeps: []
4438
+ }, this.logger).length === 1) {
4439
+ this.logger.info(`${packageName} — ${depName} linked from the server closure`, { meta: { closureVersion } });
4440
+ linked.push(depName);
4441
+ continue;
4442
+ }
4443
+ }
4444
+ needsInstall = true;
4445
+ }
4446
+ return {
4447
+ linked,
4448
+ needsInstall
4449
+ };
4450
+ }
4451
+ /**
4452
+ * npm-install an installed addon's runtime deps IN PLACE (the restore /
4453
+ * repair path — the tree is not yet running at boot), then the same tail
4454
+ * every fresh install gets: closure dedupe + native deps + prebuild ensure.
4455
+ * Closure links are stripped first so npm reifies against plain missing
4456
+ * dirs instead of symlinks it does not own.
4457
+ */
4458
+ async reinstallAddonDeps(addonDir, packageName) {
4459
+ const closureNodeModules = resolveClosureNodeModulesDir(this.closureNodeModulesDir);
4460
+ if (closureNodeModules !== null) {
4461
+ const state = collectAddonTreeState(addonDir);
4462
+ for (const link of state.existingLinks) if (isClosureLinkTarget(link.target, closureNodeModules)) node_fs.rmSync(node_path.join(addonDir, "node_modules", link.name), { force: true });
4463
+ }
4464
+ this.logger.info(`${packageName} — reinstalling runtime dependencies`, { meta: { addonDir } });
4465
+ await require_manifest_system_deps.runNpm([
4466
+ "install",
4467
+ "--omit=dev",
4468
+ "--omit=peer",
4469
+ "--no-audit",
4470
+ "--no-fund",
4471
+ "--no-package-lock",
4472
+ ...this.registry ? ["--registry", this.registry] : []
4473
+ ], this.npmRunOptions(addonDir, 24e4));
4474
+ this.dedupeTreeAgainstClosure(addonDir, packageName);
4475
+ await this.installNativeDependencies(addonDir, packageName);
4476
+ }
4477
+ /**
3987
4478
  * Install a single addon — on-demand installer used by AddonPackageService
3988
4479
  * (admin UI "Install addon" / "Reinstall" buttons).
3989
4480
  *
@@ -4091,6 +4582,7 @@ var AddonInstaller = class AddonInstaller {
4091
4582
  logger: this.logger,
4092
4583
  bundledSourceDir: sourceDir
4093
4584
  });
4585
+ this.dedupeTreeAgainstClosure(targetDir, packageName);
4094
4586
  this.logger.info(`${packageName} — copied from ${sourceDir}`);
4095
4587
  return {
4096
4588
  name: pkgData.name,
@@ -4394,6 +4886,40 @@ var AddonInstaller = class AddonInstaller {
4394
4886
  logger: this.logger
4395
4887
  });
4396
4888
  }
4889
+ /**
4890
+ * Stop copying what the server closure already provides: replace each
4891
+ * top-level `node_modules` entry the closure serves at a COMPATIBLE version
4892
+ * with a symlink into the closure (see `deps/closure-dedupe.ts` for the
4893
+ * plan rules and the live measurements). Runs LAST in tree
4894
+ * materialisation — after the runtime-dep npm install AND the native-dep
4895
+ * steps — so no later npm invocation can fight the links.
4896
+ *
4897
+ * Non-fatal by design: dedupe is an optimisation, and its failure mode is
4898
+ * "the addon keeps its own copies", which is exactly today's behaviour. A
4899
+ * refusal on version grounds is logged loudly — that line is the answer to
4900
+ * "why does this addon still carry its own zod".
4901
+ */
4902
+ dedupeTreeAgainstClosure(pkgDir, packageName) {
4903
+ const closureNodeModules = resolveClosureNodeModulesDir(this.closureNodeModulesDir);
4904
+ if (closureNodeModules === null) return;
4905
+ try {
4906
+ const state = collectAddonTreeState(pkgDir);
4907
+ if (state.candidates.length === 0) return;
4908
+ const pkgView = readPackageJson(node_path.join(pkgDir, "package.json"));
4909
+ const plan = planClosureDedupe({
4910
+ candidates: state.candidates,
4911
+ closureVersions: readClosureVersions(closureNodeModules, state.candidates.map((c) => c.name)),
4912
+ requirements: state.requirements,
4913
+ neverLink: readNativeDependencyNames(pkgView?.raw)
4914
+ });
4915
+ const linked = executeClosureDedupe(pkgDir, closureNodeModules, plan, this.logger);
4916
+ if (linked.length > 0) this.logger.info(`${packageName} — ${linked.length} node_modules entr${linked.length === 1 ? "y" : "ies"} now resolve from the server closure`, { meta: { linked } });
4917
+ const refused = plan.keeps.filter((k) => k.reason === "version-incompatible");
4918
+ if (refused.length > 0) this.logger.info(`${packageName} — kept own copy where the closure version is incompatible`, { meta: { kept: refused.map((k) => k.name) } });
4919
+ } catch (err) {
4920
+ this.logger.warn(`${packageName} — closure dedupe failed (copies kept)`, { meta: { error: require_dist.errMsg(err) } });
4921
+ }
4922
+ }
4397
4923
  /** Install addon from a tgz file (uploaded or downloaded) */
4398
4924
  async installFromTgz(tgzPath) {
4399
4925
  const stagingRoot = node_path.join(this.addonsDir, ".staging");
@@ -4419,6 +4945,7 @@ var AddonInstaller = class AddonInstaller {
4419
4945
  const targetDir = node_path.join(this.addonsDir, pkgView.name);
4420
4946
  await this.installRuntimeDependencies(pkgDir, pkgView.name);
4421
4947
  await this.installNativeDependencies(pkgDir, pkgView.name);
4948
+ this.dedupeTreeAgainstClosure(pkgDir, pkgView.name);
4422
4949
  await this.evictInstallDir(targetDir);
4423
4950
  ensureDir(node_path.dirname(targetDir));
4424
4951
  await node_fs.promises.rename(pkgDir, targetDir);
@@ -4551,6 +5078,7 @@ var AddonInstaller = class AddonInstaller {
4551
5078
  if (!node_fs.existsSync(addonDir)) throw new Error(`Cannot update ${packageName}: install dir ${addonDir} missing`);
4552
5079
  await this.installRuntimeDependencies(stagedPath, packageName);
4553
5080
  await this.installNativeDependencies(stagedPath, packageName);
5081
+ this.dedupeTreeAgainstClosure(stagedPath, packageName);
4554
5082
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4555
5083
  const backupRoot = node_path.join(this.addonsDir, ".backups", packageName);
4556
5084
  ensureDir(backupRoot);