@camstack/system 1.2.81 → 1.2.82
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.
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +1 -1
- package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{dist-CA2mim74.js → dist-DUDXaubL.js} +144 -6
- package/dist/{dist-DZOZtn-0.mjs → dist-DpL4mUA4.mjs} +144 -6
- package/dist/index.js +165 -36
- package/dist/index.mjs +165 -36
- package/dist/kernel/addon-installer.d.ts +54 -2
- package/dist/kernel/addon-loader.d.ts +33 -0
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as __toCommonJS, i as __require, n as __esmMin, o as __toESM$1, r as __exportAll, t as __commonJSMin$1 } from "./chunk-CNf5ZN-e.mjs";
|
|
2
|
-
import { $ as objectInputDeclaresAddonId, At as emitDownForOwnedCaps, B as isArrayOutputSchema, Bt as scopeKey, Ct as ReadinessRegistry, Dt as asNumber, Et as asJsonObject$1, G as kebabToCamel, H as isObjectInput, Ht as EventCategory$1, It as parseJsonObject, K as lifecycleJobSchema, Lt as parseJsonUnknown$1, Mt as expandCapMethods, Ot as asString$1, R as extractNestedAddonId, Rt as readinessKey, V as isCollectionArrayMethod, W as isVoidInput, X as looseSchema, Y as logLevelAtMost, _t as DATAPLANE_SECRET_HEADER$1, d as METHOD_ACCESS_MAP, f as RUNTIME_DEFAULTS, ht as errMsg$1, it as scopesAllowDeviceCap, kt as createEvent, nt as procedureAuthKey, rt as scopesAllowAddon, t as ALL_CAPABILITY_DEFINITIONS, vt as DEVICE_SETTINGS_CONTRIBUTION_METHODS, wt as ReadinessTimeoutError, y as addonSettingsCapability, yt as DEVICE_STATUS_METHOD, zt as resolveCapMount } from "./dist-
|
|
2
|
+
import { $ as objectInputDeclaresAddonId, At as emitDownForOwnedCaps, B as isArrayOutputSchema, Bt as scopeKey, Ct as ReadinessRegistry, Dt as asNumber, Et as asJsonObject$1, G as kebabToCamel, H as isObjectInput, Ht as EventCategory$1, It as parseJsonObject, K as lifecycleJobSchema, Lt as parseJsonUnknown$1, Mt as expandCapMethods, Ot as asString$1, R as extractNestedAddonId, Rt as readinessKey, V as isCollectionArrayMethod, W as isVoidInput, X as looseSchema, Y as logLevelAtMost, _t as DATAPLANE_SECRET_HEADER$1, d as METHOD_ACCESS_MAP, f as RUNTIME_DEFAULTS, ht as errMsg$1, it as scopesAllowDeviceCap, kt as createEvent, nt as procedureAuthKey, rt as scopesAllowAddon, t as ALL_CAPABILITY_DEFINITIONS, vt as DEVICE_SETTINGS_CONTRIBUTION_METHODS, wt as ReadinessTimeoutError, y as addonSettingsCapability, yt as DEVICE_STATUS_METHOD, zt as resolveCapMount } from "./dist-DpL4mUA4.mjs";
|
|
3
3
|
import { AlertCenterAddon } from "./builtins/alerts/alerts.addon.mjs";
|
|
4
4
|
import "./builtins/alerts/index.mjs";
|
|
5
5
|
import { t as formatLogLine } from "./formatter-B7qW8bPJ.mjs";
|
|
@@ -3064,6 +3064,74 @@ function isRecord$1(value) {
|
|
|
3064
3064
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3065
3065
|
}
|
|
3066
3066
|
/**
|
|
3067
|
+
* How far up from the entry file to look for the owning `package.json`
|
|
3068
|
+
* (`<pkg>/dist/chunks/x.js` → 3 hops). Bounded so a loose module outside a
|
|
3069
|
+
* package can't walk to the filesystem root.
|
|
3070
|
+
*/
|
|
3071
|
+
var PACKAGE_JSON_LOOKUP_DEPTH = 4;
|
|
3072
|
+
/** Version from the `package.json` that owns `entryPath`, or null. */
|
|
3073
|
+
function readOwningPackageVersion(entryPath) {
|
|
3074
|
+
let dir = path$39.dirname(entryPath);
|
|
3075
|
+
for (let hop = 0; hop < PACKAGE_JSON_LOOKUP_DEPTH; hop++) {
|
|
3076
|
+
try {
|
|
3077
|
+
const version = asJsonObject$1(parseJsonUnknown$1(fs$17.readFileSync(path$39.join(dir, "package.json"), "utf-8")))?.["version"];
|
|
3078
|
+
if (typeof version === "string" && version.length > 0) return version;
|
|
3079
|
+
} catch {}
|
|
3080
|
+
const parent = path$39.dirname(dir);
|
|
3081
|
+
if (parent === dir) break;
|
|
3082
|
+
dir = parent;
|
|
3083
|
+
}
|
|
3084
|
+
return null;
|
|
3085
|
+
}
|
|
3086
|
+
/**
|
|
3087
|
+
* Cache-bust key for an addon entry module: the `?v=` query that makes each
|
|
3088
|
+
* on-disk REVISION of `entryPath` a distinct ESM module URL. Returns null when
|
|
3089
|
+
* the entry cannot be stat'd — the caller then does a plain import.
|
|
3090
|
+
*
|
|
3091
|
+
* The key is `<version>-<content hash>`, and BOTH halves are load-bearing
|
|
3092
|
+
* because each on its own has a documented way of not changing across a real
|
|
3093
|
+
* redeploy:
|
|
3094
|
+
*
|
|
3095
|
+
* - **mtime does not change.** `npm pack` normalises every file's mtime in the
|
|
3096
|
+
* tarball to 499162500000 (1985-10-26); every install path here extracts
|
|
3097
|
+
* such a tarball, so the entry file of v1.0.0 and of v1.0.99 carry the same
|
|
3098
|
+
* mtime. An mtime-only key (what this used to be) is a NO-OP — it produced
|
|
3099
|
+
* one byte-identical URL for every version of every addon ever installed.
|
|
3100
|
+
* - **the version does not change** on a `camstack deploy` of an unbumped
|
|
3101
|
+
* workspace package — the whole dev loop.
|
|
3102
|
+
* - **the entry hash does not change** when a multi-chunk dist alters a chunk
|
|
3103
|
+
* the entry only re-exports; the version bump covers that case.
|
|
3104
|
+
*
|
|
3105
|
+
* Hashing the entry costs one extra read of an already-read file (~1.5 MB for
|
|
3106
|
+
* the largest addon bundle, single-digit ms) once per addon load. Cheap against
|
|
3107
|
+
* what a stale key costs: hub-main keeps validating `addons.custom` input
|
|
3108
|
+
* against the OLD `customActions` schema — silently STRIPPING newly-added
|
|
3109
|
+
* fields — and, worse, Node caches a load that THREW as a rejected module and
|
|
3110
|
+
* replays that rejection for every later retry at the same URL. On 2026-08-12
|
|
3111
|
+
* `@camstack/addon-export-hap` burned 126 identical failed load retries over
|
|
3112
|
+
* 10.5h, none of which could have succeeded: the fixed bytes on disk were never
|
|
3113
|
+
* read.
|
|
3114
|
+
*
|
|
3115
|
+
* Never throws: an unreadable version degrades to the mtime, an unreadable body
|
|
3116
|
+
* to mtime+size.
|
|
3117
|
+
*/
|
|
3118
|
+
function addonModuleCacheKey(entryPath) {
|
|
3119
|
+
let stat;
|
|
3120
|
+
try {
|
|
3121
|
+
stat = fs$17.statSync(entryPath);
|
|
3122
|
+
} catch {
|
|
3123
|
+
return null;
|
|
3124
|
+
}
|
|
3125
|
+
const version = readOwningPackageVersion(entryPath) ?? `m${stat.mtimeMs}`;
|
|
3126
|
+
let content;
|
|
3127
|
+
try {
|
|
3128
|
+
content = createHash("sha1").update(fs$17.readFileSync(entryPath)).digest("hex").slice(0, 16);
|
|
3129
|
+
} catch {
|
|
3130
|
+
content = `m${stat.mtimeMs}s${stat.size}`;
|
|
3131
|
+
}
|
|
3132
|
+
return `${version}-${content}`;
|
|
3133
|
+
}
|
|
3134
|
+
/**
|
|
3067
3135
|
* Import an addon entry module FRESH on every load, defeating Node's
|
|
3068
3136
|
* per-URL ESM module cache.
|
|
3069
3137
|
*
|
|
@@ -3072,23 +3140,15 @@ function isRecord$1(value) {
|
|
|
3072
3140
|
* redeploy returns the STALE previously-cached module. The addon RUNNER child
|
|
3073
3141
|
* is a fresh process so it reloads correctly — but hub-main also imports the
|
|
3074
3142
|
* module to read its static catalog (`customActions` Zod schemas, addon class)
|
|
3075
|
-
* for the `addons.custom` "validate on the way in" guard.
|
|
3076
|
-
* that guard keeps validating against the OLD schema, silently STRIPPING any
|
|
3077
|
-
* newly-added custom-action input field until a full hub restart (observed:
|
|
3078
|
-
* benchmark `deviceKey`/multi-device fan-out fields dropped after redeploy).
|
|
3143
|
+
* for the `addons.custom` "validate on the way in" guard.
|
|
3079
3144
|
*
|
|
3080
|
-
*
|
|
3081
|
-
*
|
|
3082
|
-
* import if the file can't be stat'd.
|
|
3145
|
+
* The distinguishing key is {@link addonModuleCacheKey}; a plain import is the
|
|
3146
|
+
* fallback when the entry cannot be stat'd.
|
|
3083
3147
|
*/
|
|
3084
3148
|
async function importAddonModuleFresh(entryPath) {
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
} catch {
|
|
3089
|
-
return import(entryPath);
|
|
3090
|
-
}
|
|
3091
|
-
return import(`${pathToFileURL(entryPath).href}?v=${bust}`);
|
|
3149
|
+
const bust = addonModuleCacheKey(entryPath);
|
|
3150
|
+
if (bust === null) return import(entryPath);
|
|
3151
|
+
return import(`${pathToFileURL(entryPath).href}?v=${encodeURIComponent(bust)}`);
|
|
3092
3152
|
}
|
|
3093
3153
|
/**
|
|
3094
3154
|
* Package.json manifest boundary: the `camstack` field is authored by third-party
|
|
@@ -4510,6 +4570,79 @@ var AddonInstaller = class AddonInstaller {
|
|
|
4510
4570
|
force: true
|
|
4511
4571
|
}).catch(() => void 0)));
|
|
4512
4572
|
}
|
|
4573
|
+
/**
|
|
4574
|
+
* Make `pkgDir` (an addon tree that has NOT been swapped in yet) runnable:
|
|
4575
|
+
* strip the host-provided deps from its manifest, then `npm install` whatever
|
|
4576
|
+
* survives.
|
|
4577
|
+
*
|
|
4578
|
+
* ONE implementation for every path that materialises an addon tree —
|
|
4579
|
+
* `installFromTgz` (deploy/upload) and `applyUpdateFromStaged` (OTA). The OTA
|
|
4580
|
+
* path did not have it, and that is a whole outage class: an addon declaring
|
|
4581
|
+
* `camstack.runtimeDependencies` was broken by its FIRST OTA update, its dist
|
|
4582
|
+
* landing without `node_modules`. Every load then threw `Cannot find module`
|
|
4583
|
+
* and the retry loop replayed it — `@camstack/addon-export-hap`, 126 failed
|
|
4584
|
+
* load retries over 10.5h on 2026-08-12; `@camstack/addon-remote-storage`
|
|
4585
|
+
* (`@aws-sdk/*`, `webdav`) is the same landmine.
|
|
4586
|
+
*
|
|
4587
|
+
* **Strip.** `@camstack/*` are host-provided system packages that resolve via
|
|
4588
|
+
* the host's node_modules (Node walks up from the installed addon dir). Left
|
|
4589
|
+
* in, this `npm install` pulls the LAST PUBLISHED `@camstack/types` (etc.)
|
|
4590
|
+
* into the addon's OWN node_modules where it SHADOWS the host's fresh copy —
|
|
4591
|
+
* local additions to a system package silently never reach the deployed
|
|
4592
|
+
* addon. Addon dists are self-contained besides, so `stripBundledDeps` keeps
|
|
4593
|
+
* only what the manifest explicitly declares in
|
|
4594
|
+
* `camstack.runtimeDependencies` (see fs-utils / ADR-0027). Nothing left to
|
|
4595
|
+
* install is the COMMON case: skip npm rather than pay an empty round-trip.
|
|
4596
|
+
*
|
|
4597
|
+
* **Failure is FATAL.** This used to warn-and-continue on the tgz path ("the
|
|
4598
|
+
* addon may still load if imports are lazy") — live cost: a host with no npm
|
|
4599
|
+
* (packaged Electron Mac agent) installed every runtime-dep addon WITHOUT its
|
|
4600
|
+
* node_modules for DAYS; each reported "running" while its entry failed at
|
|
4601
|
+
* import (`Cannot find module 'sharp'`), the detection group ran 1/3 members,
|
|
4602
|
+
* and the node silently dropped out of the balancer. Throwing here is what
|
|
4603
|
+
* keeps the PREVIOUS install intact: every caller builds the tree before it
|
|
4604
|
+
* swaps, so an abort simply means the swap never happens.
|
|
4605
|
+
*
|
|
4606
|
+
* A missing/unreadable manifest is NOT fatal — it is logged and skipped. The
|
|
4607
|
+
* callers validate the tree themselves, and a tree with no readable
|
|
4608
|
+
* package.json has no declared deps to install (the loader will skip the
|
|
4609
|
+
* package entirely).
|
|
4610
|
+
*/
|
|
4611
|
+
async installRuntimeDependencies(pkgDir, packageName) {
|
|
4612
|
+
const pkgJsonPath = path$39.join(pkgDir, "package.json");
|
|
4613
|
+
const pkgView = readPackageJson(pkgJsonPath);
|
|
4614
|
+
if (pkgView == null) {
|
|
4615
|
+
this.logger.error(`${packageName} — no readable package.json in the prepared tree; runtime dependencies NOT installed`, { meta: { pkgJsonPath } });
|
|
4616
|
+
return;
|
|
4617
|
+
}
|
|
4618
|
+
const strippedManifest = stripBundledDeps(pkgView.raw);
|
|
4619
|
+
await fs$17.promises.writeFile(pkgJsonPath, JSON.stringify(strippedManifest, null, 2));
|
|
4620
|
+
const deps = asJsonObject$1(strippedManifest["dependencies"]);
|
|
4621
|
+
const depNames = deps ? Object.keys(deps) : [];
|
|
4622
|
+
if (depNames.length === 0) return;
|
|
4623
|
+
this.logger.info(`${packageName} — installing runtime dependencies`, { meta: {
|
|
4624
|
+
stagingDir: pkgDir,
|
|
4625
|
+
dependencies: depNames
|
|
4626
|
+
} });
|
|
4627
|
+
try {
|
|
4628
|
+
await runNpm([
|
|
4629
|
+
"install",
|
|
4630
|
+
"--omit=dev",
|
|
4631
|
+
"--omit=peer",
|
|
4632
|
+
"--no-audit",
|
|
4633
|
+
"--no-fund",
|
|
4634
|
+
"--no-package-lock",
|
|
4635
|
+
...this.registry ? ["--registry", this.registry] : []
|
|
4636
|
+
], this.npmRunOptions(pkgDir, 24e4));
|
|
4637
|
+
} catch (err) {
|
|
4638
|
+
this.logger.error(`${packageName} — runtime dependency install FAILED; update ABORTED (a swapped-in copy would fail every load with "Cannot find module")`, { meta: {
|
|
4639
|
+
pkgDir,
|
|
4640
|
+
dependencies: depNames,
|
|
4641
|
+
error: errMsg$1(err)
|
|
4642
|
+
} });
|
|
4643
|
+
throw new Error(`${packageName} — runtime dependency install failed: ${errMsg$1(err)}`, { cause: err });
|
|
4644
|
+
}
|
|
4645
|
+
}
|
|
4513
4646
|
/** Install addon from a tgz file (uploaded or downloaded) */
|
|
4514
4647
|
async installFromTgz(tgzPath) {
|
|
4515
4648
|
const stagingRoot = path$39.join(this.addonsDir, ".staging");
|
|
@@ -4533,25 +4666,7 @@ var AddonInstaller = class AddonInstaller {
|
|
|
4533
4666
|
if (!pkgView) throw new Error(`Invalid package.json at ${pkgJsonPath}`);
|
|
4534
4667
|
if (!pkgView.camstackAddons) throw new Error(`Package ${pkgView.name} has no camstack.addons manifest`);
|
|
4535
4668
|
const targetDir = path$39.join(this.addonsDir, pkgView.name);
|
|
4536
|
-
|
|
4537
|
-
await fs$17.promises.writeFile(pkgJsonPath, JSON.stringify(strippedManifest, null, 2));
|
|
4538
|
-
const strippedRuntimeDeps = strippedManifest["dependencies"];
|
|
4539
|
-
if (strippedRuntimeDeps != null && typeof strippedRuntimeDeps === "object" && Object.keys(strippedRuntimeDeps).length > 0) {
|
|
4540
|
-
this.logger.info(`${pkgView.name} — installing runtime dependencies`, { meta: { stagingDir: pkgDir } });
|
|
4541
|
-
try {
|
|
4542
|
-
await runNpm([
|
|
4543
|
-
"install",
|
|
4544
|
-
"--omit=dev",
|
|
4545
|
-
"--omit=peer",
|
|
4546
|
-
"--no-audit",
|
|
4547
|
-
"--no-fund",
|
|
4548
|
-
"--no-package-lock",
|
|
4549
|
-
...this.registry ? ["--registry", this.registry] : []
|
|
4550
|
-
], this.npmRunOptions(pkgDir, 24e4));
|
|
4551
|
-
} catch (err) {
|
|
4552
|
-
throw new Error(`${pkgView.name} — runtime dependency install failed: ${errMsg$1(err)}`, { cause: err });
|
|
4553
|
-
}
|
|
4554
|
-
}
|
|
4669
|
+
await this.installRuntimeDependencies(pkgDir, pkgView.name);
|
|
4555
4670
|
try {
|
|
4556
4671
|
await installManifestNativeDeps(pkgDir, pkgView.raw, this.logger, this.registry, this.npmCacheDir);
|
|
4557
4672
|
} catch (nativeErr) {
|
|
@@ -4663,18 +4778,32 @@ var AddonInstaller = class AddonInstaller {
|
|
|
4663
4778
|
* replaces step 2 ("install new version") with an atomic directory
|
|
4664
4779
|
* move of `stagedPath` into `addonDir`.
|
|
4665
4780
|
*
|
|
4781
|
+
* "Skips npm entirely" is true only of the REGISTRY round-trip: the staged
|
|
4782
|
+
* tree is the raw unpacked tarball, so the addon's own
|
|
4783
|
+
* `camstack.runtimeDependencies` still have to be installed into it, exactly
|
|
4784
|
+
* as `installFromTgz` does. Skipping that is what broke every runtime-dep
|
|
4785
|
+
* addon on its first OTA update — see {@link installRuntimeDependencies}.
|
|
4786
|
+
*
|
|
4787
|
+
* Order matters and is `installFromTgz`'s: the tree is COMPLETED FIRST, and
|
|
4788
|
+
* the live install is only then backed up and replaced. So the addon keeps
|
|
4789
|
+
* serving the old version for the whole (minutes-long) npm install, the swap
|
|
4790
|
+
* window stays two renames wide, and a dependency failure aborts with the
|
|
4791
|
+
* previous version untouched instead of leaving a dependency-less copy live.
|
|
4792
|
+
*
|
|
4666
4793
|
* On cross-device move (EXDEV) falls back to a recursive copy followed
|
|
4667
4794
|
* by removal of the staged directory. On any swap failure the backup is
|
|
4668
4795
|
* restored and the error is rethrown — identical recovery to `applyUpdate`.
|
|
4669
4796
|
*
|
|
4670
|
-
* Throws if the package is not tracked in the manifest
|
|
4671
|
-
* is missing (same preconditions as `applyUpdate`)
|
|
4797
|
+
* Throws if the package is not tracked in the manifest, if `addonDir`
|
|
4798
|
+
* is missing (same preconditions as `applyUpdate`), or if the runtime-dep
|
|
4799
|
+
* install fails.
|
|
4672
4800
|
*/
|
|
4673
4801
|
async applyUpdateFromStaged(packageName, version, stagedPath) {
|
|
4674
4802
|
const current = this.manifest.get(packageName);
|
|
4675
4803
|
if (current == null) throw new Error(`Cannot update ${packageName}: not currently tracked in manifest`);
|
|
4676
4804
|
const addonDir = path$39.join(this.addonsDir, packageName);
|
|
4677
4805
|
if (!fs$17.existsSync(addonDir)) throw new Error(`Cannot update ${packageName}: install dir ${addonDir} missing`);
|
|
4806
|
+
await this.installRuntimeDependencies(stagedPath, packageName);
|
|
4678
4807
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
4679
4808
|
const backupRoot = path$39.join(this.addonsDir, ".backups", packageName);
|
|
4680
4809
|
ensureDir(backupRoot);
|
|
@@ -302,6 +302,45 @@ export declare class AddonInstaller {
|
|
|
302
302
|
private sweepStagingResidue;
|
|
303
303
|
/** Best-effort removal of leftover `.evicting/*` dirs from earlier deploys. */
|
|
304
304
|
private sweepGraveyard;
|
|
305
|
+
/**
|
|
306
|
+
* Make `pkgDir` (an addon tree that has NOT been swapped in yet) runnable:
|
|
307
|
+
* strip the host-provided deps from its manifest, then `npm install` whatever
|
|
308
|
+
* survives.
|
|
309
|
+
*
|
|
310
|
+
* ONE implementation for every path that materialises an addon tree —
|
|
311
|
+
* `installFromTgz` (deploy/upload) and `applyUpdateFromStaged` (OTA). The OTA
|
|
312
|
+
* path did not have it, and that is a whole outage class: an addon declaring
|
|
313
|
+
* `camstack.runtimeDependencies` was broken by its FIRST OTA update, its dist
|
|
314
|
+
* landing without `node_modules`. Every load then threw `Cannot find module`
|
|
315
|
+
* and the retry loop replayed it — `@camstack/addon-export-hap`, 126 failed
|
|
316
|
+
* load retries over 10.5h on 2026-08-12; `@camstack/addon-remote-storage`
|
|
317
|
+
* (`@aws-sdk/*`, `webdav`) is the same landmine.
|
|
318
|
+
*
|
|
319
|
+
* **Strip.** `@camstack/*` are host-provided system packages that resolve via
|
|
320
|
+
* the host's node_modules (Node walks up from the installed addon dir). Left
|
|
321
|
+
* in, this `npm install` pulls the LAST PUBLISHED `@camstack/types` (etc.)
|
|
322
|
+
* into the addon's OWN node_modules where it SHADOWS the host's fresh copy —
|
|
323
|
+
* local additions to a system package silently never reach the deployed
|
|
324
|
+
* addon. Addon dists are self-contained besides, so `stripBundledDeps` keeps
|
|
325
|
+
* only what the manifest explicitly declares in
|
|
326
|
+
* `camstack.runtimeDependencies` (see fs-utils / ADR-0027). Nothing left to
|
|
327
|
+
* install is the COMMON case: skip npm rather than pay an empty round-trip.
|
|
328
|
+
*
|
|
329
|
+
* **Failure is FATAL.** This used to warn-and-continue on the tgz path ("the
|
|
330
|
+
* addon may still load if imports are lazy") — live cost: a host with no npm
|
|
331
|
+
* (packaged Electron Mac agent) installed every runtime-dep addon WITHOUT its
|
|
332
|
+
* node_modules for DAYS; each reported "running" while its entry failed at
|
|
333
|
+
* import (`Cannot find module 'sharp'`), the detection group ran 1/3 members,
|
|
334
|
+
* and the node silently dropped out of the balancer. Throwing here is what
|
|
335
|
+
* keeps the PREVIOUS install intact: every caller builds the tree before it
|
|
336
|
+
* swaps, so an abort simply means the swap never happens.
|
|
337
|
+
*
|
|
338
|
+
* A missing/unreadable manifest is NOT fatal — it is logged and skipped. The
|
|
339
|
+
* callers validate the tree themselves, and a tree with no readable
|
|
340
|
+
* package.json has no declared deps to install (the loader will skip the
|
|
341
|
+
* package entirely).
|
|
342
|
+
*/
|
|
343
|
+
private installRuntimeDependencies;
|
|
305
344
|
/** Install addon from a tgz file (uploaded or downloaded) */
|
|
306
345
|
installFromTgz(tgzPath: string): Promise<{
|
|
307
346
|
name: string;
|
|
@@ -340,12 +379,25 @@ export declare class AddonInstaller {
|
|
|
340
379
|
* replaces step 2 ("install new version") with an atomic directory
|
|
341
380
|
* move of `stagedPath` into `addonDir`.
|
|
342
381
|
*
|
|
382
|
+
* "Skips npm entirely" is true only of the REGISTRY round-trip: the staged
|
|
383
|
+
* tree is the raw unpacked tarball, so the addon's own
|
|
384
|
+
* `camstack.runtimeDependencies` still have to be installed into it, exactly
|
|
385
|
+
* as `installFromTgz` does. Skipping that is what broke every runtime-dep
|
|
386
|
+
* addon on its first OTA update — see {@link installRuntimeDependencies}.
|
|
387
|
+
*
|
|
388
|
+
* Order matters and is `installFromTgz`'s: the tree is COMPLETED FIRST, and
|
|
389
|
+
* the live install is only then backed up and replaced. So the addon keeps
|
|
390
|
+
* serving the old version for the whole (minutes-long) npm install, the swap
|
|
391
|
+
* window stays two renames wide, and a dependency failure aborts with the
|
|
392
|
+
* previous version untouched instead of leaving a dependency-less copy live.
|
|
393
|
+
*
|
|
343
394
|
* On cross-device move (EXDEV) falls back to a recursive copy followed
|
|
344
395
|
* by removal of the staged directory. On any swap failure the backup is
|
|
345
396
|
* restored and the error is rethrown — identical recovery to `applyUpdate`.
|
|
346
397
|
*
|
|
347
|
-
* Throws if the package is not tracked in the manifest
|
|
348
|
-
* is missing (same preconditions as `applyUpdate`)
|
|
398
|
+
* Throws if the package is not tracked in the manifest, if `addonDir`
|
|
399
|
+
* is missing (same preconditions as `applyUpdate`), or if the runtime-dep
|
|
400
|
+
* install fails.
|
|
349
401
|
*/
|
|
350
402
|
applyUpdateFromStaged(packageName: string, version: string, stagedPath: string): Promise<{
|
|
351
403
|
name: string;
|
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
import { AddonDeclaration, ICamstackAddon, IScopedLogger, StorageLocationDeclaration } from '@camstack/types';
|
|
2
|
+
/**
|
|
3
|
+
* Cache-bust key for an addon entry module: the `?v=` query that makes each
|
|
4
|
+
* on-disk REVISION of `entryPath` a distinct ESM module URL. Returns null when
|
|
5
|
+
* the entry cannot be stat'd — the caller then does a plain import.
|
|
6
|
+
*
|
|
7
|
+
* The key is `<version>-<content hash>`, and BOTH halves are load-bearing
|
|
8
|
+
* because each on its own has a documented way of not changing across a real
|
|
9
|
+
* redeploy:
|
|
10
|
+
*
|
|
11
|
+
* - **mtime does not change.** `npm pack` normalises every file's mtime in the
|
|
12
|
+
* tarball to 499162500000 (1985-10-26); every install path here extracts
|
|
13
|
+
* such a tarball, so the entry file of v1.0.0 and of v1.0.99 carry the same
|
|
14
|
+
* mtime. An mtime-only key (what this used to be) is a NO-OP — it produced
|
|
15
|
+
* one byte-identical URL for every version of every addon ever installed.
|
|
16
|
+
* - **the version does not change** on a `camstack deploy` of an unbumped
|
|
17
|
+
* workspace package — the whole dev loop.
|
|
18
|
+
* - **the entry hash does not change** when a multi-chunk dist alters a chunk
|
|
19
|
+
* the entry only re-exports; the version bump covers that case.
|
|
20
|
+
*
|
|
21
|
+
* Hashing the entry costs one extra read of an already-read file (~1.5 MB for
|
|
22
|
+
* the largest addon bundle, single-digit ms) once per addon load. Cheap against
|
|
23
|
+
* what a stale key costs: hub-main keeps validating `addons.custom` input
|
|
24
|
+
* against the OLD `customActions` schema — silently STRIPPING newly-added
|
|
25
|
+
* fields — and, worse, Node caches a load that THREW as a rejected module and
|
|
26
|
+
* replays that rejection for every later retry at the same URL. On 2026-08-12
|
|
27
|
+
* `@camstack/addon-export-hap` burned 126 identical failed load retries over
|
|
28
|
+
* 10.5h, none of which could have succeeded: the fixed bytes on disk were never
|
|
29
|
+
* read.
|
|
30
|
+
*
|
|
31
|
+
* Never throws: an unreadable version degrades to the mtime, an unreadable body
|
|
32
|
+
* to mtime+size.
|
|
33
|
+
*/
|
|
34
|
+
export declare function addonModuleCacheKey(entryPath: string): string | null;
|
|
2
35
|
export interface RegisteredAddon {
|
|
3
36
|
readonly declaration: AddonDeclaration;
|
|
4
37
|
readonly packageName: string;
|