@camstack/system 1.2.58 → 1.2.59
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 +2 -2
- package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
- package/dist/builtins/doorbell/binding-mirror.d.ts +65 -0
- package/dist/builtins/doorbell/virtual-doorbell.addon.d.ts +56 -11
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +248 -26
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +248 -26
- 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 +2 -2
- package/dist/builtins/snapshot/index.mjs +2 -2
- 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/retired-settings-keys.d.ts +94 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +142 -2
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +142 -2
- 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-UrXyJ9Zm.mjs → dist-BqJJWCS8.mjs} +369 -24
- package/dist/{dist-Chl2MvMd.js → dist-CV6XTApk.js} +380 -23
- package/dist/index.js +187 -39
- package/dist/index.mjs +187 -39
- package/dist/kernel/addon-installer.d.ts +55 -7
- package/dist/kernel/addon-manifest.d.ts +36 -16
- 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-
|
|
3
|
+
const require_dist = require("./dist-CV6XTApk.js");
|
|
4
4
|
const require_model_download_service = require("./model-download-service-Cpc6SWzV.js");
|
|
5
5
|
const require_manifest_python_deps = require("./manifest-python-deps-d4acYXVV.js");
|
|
6
6
|
const require_resource_monitor = require("./resource-monitor-DNNomR-i.js");
|
|
@@ -3231,6 +3231,21 @@ async function installPackageFromNpm(packageName, targetDir) {
|
|
|
3231
3231
|
//#region src/kernel/addon-manifest.ts
|
|
3232
3232
|
var MANIFEST_FILENAME = "manifest.json";
|
|
3233
3233
|
var MANIFEST_VERSION = 1;
|
|
3234
|
+
/**
|
|
3235
|
+
* Is `p` a directory, FOLLOWING symlinks?
|
|
3236
|
+
*
|
|
3237
|
+
* `Dirent.isDirectory()` answers false for a symlink, and dev installs
|
|
3238
|
+
* (`installSymlink`) point `addonsDir/@camstack/<pkg>` straight at the
|
|
3239
|
+
* workspace source. Trusting the dirent there finds zero addons on disk, which
|
|
3240
|
+
* with pruning would empty a developer's manifest on the next boot.
|
|
3241
|
+
*/
|
|
3242
|
+
function isDirLike(p) {
|
|
3243
|
+
try {
|
|
3244
|
+
return node_fs.statSync(p).isDirectory();
|
|
3245
|
+
} catch {
|
|
3246
|
+
return false;
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3234
3249
|
var AddonManifest = class {
|
|
3235
3250
|
manifestPath;
|
|
3236
3251
|
cache = null;
|
|
@@ -3320,35 +3335,48 @@ var AddonManifest = class {
|
|
|
3320
3335
|
this.cache = null;
|
|
3321
3336
|
}
|
|
3322
3337
|
/**
|
|
3323
|
-
* Reconcile the manifest
|
|
3338
|
+
* Reconcile the manifest against what is actually present on disk.
|
|
3324
3339
|
*
|
|
3325
|
-
*
|
|
3326
|
-
*
|
|
3327
|
-
*
|
|
3328
|
-
* version and timestamps are authoritative — a later install/update
|
|
3329
|
-
* overwrites them through `upsert`).
|
|
3340
|
+
* **The directory is the truth.** It is what `AddonLoader` runs; a manifest
|
|
3341
|
+
* that disagrees with it is a lie the update flow and the admin UI then
|
|
3342
|
+
* repeat. Three corrections, all idempotent and safe on every boot:
|
|
3330
3343
|
*
|
|
3331
|
-
*
|
|
3332
|
-
*
|
|
3333
|
-
*
|
|
3334
|
-
* present and skips, never calling `upsert`)
|
|
3335
|
-
*
|
|
3336
|
-
*
|
|
3337
|
-
*
|
|
3338
|
-
*
|
|
3344
|
+
* - **added** — an on-disk addon with no entry gets one, source `'seed'`.
|
|
3345
|
+
* Addons baked into the Docker/Electron image are copied straight into
|
|
3346
|
+
* `addonsDir` by the container entrypoint (`ensureRequiredPackages` then
|
|
3347
|
+
* sees `package.json` present and skips, never calling `upsert`), so they
|
|
3348
|
+
* would otherwise stay untracked and `applyUpdate` would reject them with
|
|
3349
|
+
* "not currently tracked in manifest" — what broke "Update all" on a
|
|
3350
|
+
* fresh image.
|
|
3351
|
+
* - **version-corrected** — an entry whose version disagrees with the
|
|
3352
|
+
* installed `package.json` adopts the DISK version. Provenance (`source`,
|
|
3353
|
+
* `installedAt`) is NOT derivable from disk and stays authoritative here;
|
|
3354
|
+
* `updatedAt` is left alone because the correction is not an update, and
|
|
3355
|
+
* stamping `now` would claim an install that never happened.
|
|
3356
|
+
* - **pruned** — an entry naming a directory that does not exist. The live
|
|
3357
|
+
* hub carried three (`@camstack/system 1.2.29`, `addon-viewer-ui 1.1.39`,
|
|
3358
|
+
* `addon-agent-ui 1.2.4`); `@camstack/system` in particular is
|
|
3359
|
+
* host-provided and deliberately NOT seeded under `addonsDir`
|
|
3360
|
+
* ([D45]), so its entry advertised a copy that must not exist.
|
|
3361
|
+
* An entry holding a `lastBackupDir` is NEVER pruned: that pointer is the
|
|
3362
|
+
* only reference to a pre-update backup, and the launcher's backup sweep
|
|
3363
|
+
* reclaims anything the manifest does not point at.
|
|
3339
3364
|
*
|
|
3340
|
-
*
|
|
3365
|
+
* `onChange` receives one record per correction, so the caller can log
|
|
3366
|
+
* exactly what moved. Returns the total number of changes.
|
|
3341
3367
|
*/
|
|
3342
|
-
reconcileFromDisk(addonsDir) {
|
|
3368
|
+
reconcileFromDisk(addonsDir, onChange) {
|
|
3343
3369
|
if (!node_fs.existsSync(addonsDir)) return 0;
|
|
3344
3370
|
const m = this.read();
|
|
3345
|
-
let
|
|
3371
|
+
let changes = 0;
|
|
3346
3372
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3373
|
+
/** Package name → version, for every ADDON package actually installed. */
|
|
3374
|
+
const onDisk = /* @__PURE__ */ new Map();
|
|
3347
3375
|
for (const scope of node_fs.readdirSync(addonsDir, { withFileTypes: true })) {
|
|
3348
|
-
if (!scope.
|
|
3376
|
+
if (!scope.name.startsWith("@") || !isDirLike(node_path.join(addonsDir, scope.name))) continue;
|
|
3349
3377
|
const scopeDir = node_path.join(addonsDir, scope.name);
|
|
3350
3378
|
for (const pkg of node_fs.readdirSync(scopeDir, { withFileTypes: true })) {
|
|
3351
|
-
if (!pkg.
|
|
3379
|
+
if (!isDirLike(node_path.join(scopeDir, pkg.name))) continue;
|
|
3352
3380
|
const pkgJsonPath = node_path.join(scopeDir, pkg.name, "package.json");
|
|
3353
3381
|
if (!node_fs.existsSync(pkgJsonPath)) continue;
|
|
3354
3382
|
let pkgJson;
|
|
@@ -3362,22 +3390,57 @@ var AddonManifest = class {
|
|
|
3362
3390
|
if (!pkgJson.name || !pkgJson.version) continue;
|
|
3363
3391
|
const addons = pkgJson.camstack?.addons;
|
|
3364
3392
|
if (!Array.isArray(addons) || addons.length === 0) continue;
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3393
|
+
onDisk.set(pkgJson.name, pkgJson.version);
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
for (const [name, version] of onDisk) {
|
|
3397
|
+
const prev = m.addons[name];
|
|
3398
|
+
if (prev == null) {
|
|
3399
|
+
m.addons[name] = {
|
|
3400
|
+
name,
|
|
3401
|
+
version,
|
|
3369
3402
|
source: "seed",
|
|
3370
3403
|
installedAt: now,
|
|
3371
3404
|
updatedAt: now
|
|
3372
3405
|
};
|
|
3373
|
-
|
|
3406
|
+
changes++;
|
|
3407
|
+
onChange?.({
|
|
3408
|
+
kind: "added",
|
|
3409
|
+
name,
|
|
3410
|
+
to: version
|
|
3411
|
+
});
|
|
3412
|
+
continue;
|
|
3374
3413
|
}
|
|
3414
|
+
if (prev.version !== version) {
|
|
3415
|
+
m.addons[name] = {
|
|
3416
|
+
...prev,
|
|
3417
|
+
version
|
|
3418
|
+
};
|
|
3419
|
+
changes++;
|
|
3420
|
+
onChange?.({
|
|
3421
|
+
kind: "version-corrected",
|
|
3422
|
+
name,
|
|
3423
|
+
from: prev.version,
|
|
3424
|
+
to: version
|
|
3425
|
+
});
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
for (const [name, entry] of Object.entries(m.addons)) {
|
|
3429
|
+
if (onDisk.has(name)) continue;
|
|
3430
|
+
if (entry.lastBackupDir) continue;
|
|
3431
|
+
delete m.addons[name];
|
|
3432
|
+
changes++;
|
|
3433
|
+
onChange?.({
|
|
3434
|
+
kind: "pruned",
|
|
3435
|
+
name,
|
|
3436
|
+
from: entry.version
|
|
3437
|
+
});
|
|
3375
3438
|
}
|
|
3376
|
-
if (
|
|
3439
|
+
if (changes > 0) {
|
|
3377
3440
|
m.updatedAt = now;
|
|
3378
3441
|
this.write(m);
|
|
3379
3442
|
}
|
|
3380
|
-
return
|
|
3443
|
+
return changes;
|
|
3381
3444
|
}
|
|
3382
3445
|
/**
|
|
3383
3446
|
* Migration helper: if the manifest is empty but the addons directory
|
|
@@ -3760,6 +3823,34 @@ var noopLogger = {
|
|
|
3760
3823
|
return noopLogger;
|
|
3761
3824
|
}
|
|
3762
3825
|
};
|
|
3826
|
+
/**
|
|
3827
|
+
* A staging tree younger than this may belong to an install still running —
|
|
3828
|
+
* possibly in another process. Only older trees are residue worth sweeping.
|
|
3829
|
+
* Generous on purpose: an addon's runtime-dependency `npm install` on a slow
|
|
3830
|
+
* host is minutes, and the cost of leaving residue one deploy longer is disk,
|
|
3831
|
+
* while the cost of sweeping a live one is a FAILED deploy.
|
|
3832
|
+
*/
|
|
3833
|
+
var STAGING_RESIDUE_MIN_AGE_MS = 30 * 6e4;
|
|
3834
|
+
/**
|
|
3835
|
+
* Pick the staging entries that are safe to delete.
|
|
3836
|
+
*
|
|
3837
|
+
* `installFromTgz` used to clear the WHOLE `.staging` root before creating its
|
|
3838
|
+
* own dir. Two deploys arriving within one install's lifetime — the normal case
|
|
3839
|
+
* when the hub propagates two packages back to back — meant the second one
|
|
3840
|
+
* deleted the first one's tree out from under its running `npm install`:
|
|
3841
|
+
*
|
|
3842
|
+
* npm error code ENOENT
|
|
3843
|
+
* npm error path …/.staging/install-14820-2026-08-07T12-28-28-875Z/package/package.json
|
|
3844
|
+
*
|
|
3845
|
+
* which the propagation then reported as `runtime dependency install failed`.
|
|
3846
|
+
* Observed on the Mac desktop agent and on little-unraid on 2026-08-07.
|
|
3847
|
+
*
|
|
3848
|
+
* So a sweep only removes what cannot be live: not a tree this process is
|
|
3849
|
+
* building, and not one young enough to belong to anybody's in-flight install.
|
|
3850
|
+
*/
|
|
3851
|
+
function selectSweepableStaging(entries, inFlight, minAgeMs = STAGING_RESIDUE_MIN_AGE_MS) {
|
|
3852
|
+
return entries.filter((entry) => !inFlight.has(entry.name) && entry.ageMs >= minAgeMs).map((entry) => entry.name);
|
|
3853
|
+
}
|
|
3763
3854
|
var AddonInstaller = class AddonInstaller {
|
|
3764
3855
|
addonsDir;
|
|
3765
3856
|
registry;
|
|
@@ -3776,6 +3867,11 @@ var AddonInstaller = class AddonInstaller {
|
|
|
3776
3867
|
* `getAddonInstallSource`).
|
|
3777
3868
|
*/
|
|
3778
3869
|
manifest;
|
|
3870
|
+
/**
|
|
3871
|
+
* Basenames under `.staging` this process is currently building. Never swept
|
|
3872
|
+
* — see {@link selectSweepableStaging}.
|
|
3873
|
+
*/
|
|
3874
|
+
inFlightStaging = /* @__PURE__ */ new Set();
|
|
3779
3875
|
constructor(config, logger = noopLogger) {
|
|
3780
3876
|
this.addonsDir = config.addonsDir;
|
|
3781
3877
|
this.registry = config.registry;
|
|
@@ -3934,19 +4030,34 @@ var AddonInstaller = class AddonInstaller {
|
|
|
3934
4030
|
}
|
|
3935
4031
|
}
|
|
3936
4032
|
/**
|
|
3937
|
-
* Reconcile the install manifest
|
|
3938
|
-
* disk
|
|
3939
|
-
*
|
|
3940
|
-
*
|
|
3941
|
-
*
|
|
3942
|
-
*
|
|
4033
|
+
* Reconcile the install manifest against the addons actually present on
|
|
4034
|
+
* disk: register seeded addons, correct a version that disagrees with the
|
|
4035
|
+
* installed `package.json`, and drop entries naming a directory that does
|
|
4036
|
+
* not exist. Call once after `ensureRequiredPackages`, before any update
|
|
4037
|
+
* can be in flight. See `AddonManifest.reconcileFromDisk` for the rules.
|
|
4038
|
+
*
|
|
4039
|
+
* Every correction is logged by name — a manifest that silently disagreed
|
|
4040
|
+
* with disk is what made "@camstack/system 1.2.29" read as installed on a
|
|
4041
|
+
* hub that deliberately has no copy of it.
|
|
3943
4042
|
*
|
|
3944
|
-
* Returns the number of
|
|
4043
|
+
* Returns the number of changes.
|
|
3945
4044
|
*/
|
|
3946
4045
|
reconcileManifest() {
|
|
3947
|
-
const
|
|
3948
|
-
|
|
3949
|
-
|
|
4046
|
+
const changes = this.manifest.reconcileFromDisk(this.addonsDir, (c) => {
|
|
4047
|
+
switch (c.kind) {
|
|
4048
|
+
case "added":
|
|
4049
|
+
this.logger.info(`Manifest — registered seeded addon ${c.name}@${c.to}`);
|
|
4050
|
+
break;
|
|
4051
|
+
case "version-corrected":
|
|
4052
|
+
this.logger.info(`Manifest — ${c.name} recorded ${c.from} but disk has ${c.to}; adopting disk`);
|
|
4053
|
+
break;
|
|
4054
|
+
case "pruned":
|
|
4055
|
+
this.logger.info(`Manifest — dropped ${c.name}@${c.from}: no install directory on disk`);
|
|
4056
|
+
break;
|
|
4057
|
+
}
|
|
4058
|
+
});
|
|
4059
|
+
if (changes > 0) this.logger.info(`Manifest reconciled — ${changes} correction(s)`);
|
|
4060
|
+
return changes;
|
|
3950
4061
|
}
|
|
3951
4062
|
/**
|
|
3952
4063
|
* Install a single addon — on-demand installer used by AddonPackageService
|
|
@@ -4184,6 +4295,40 @@ var AddonInstaller = class AddonInstaller {
|
|
|
4184
4295
|
} });
|
|
4185
4296
|
});
|
|
4186
4297
|
}
|
|
4298
|
+
/**
|
|
4299
|
+
* Best-effort removal of `.staging` residue left by a CRASHED earlier deploy.
|
|
4300
|
+
*
|
|
4301
|
+
* Deliberately not `sweepGraveyard`: an evicted `.evicting/*` dir is dead by
|
|
4302
|
+
* construction, but a `.staging/*` dir may be an install still running — in
|
|
4303
|
+
* this process or another one. Deleting one of those fails a live deploy with
|
|
4304
|
+
* an ENOENT on its own package.json, which is what happened on 2026-08-07.
|
|
4305
|
+
*/
|
|
4306
|
+
async sweepStagingResidue(stagingRoot) {
|
|
4307
|
+
let names;
|
|
4308
|
+
try {
|
|
4309
|
+
names = node_fs.readdirSync(stagingRoot);
|
|
4310
|
+
} catch {
|
|
4311
|
+
return;
|
|
4312
|
+
}
|
|
4313
|
+
const now = Date.now();
|
|
4314
|
+
const ages = [];
|
|
4315
|
+
for (const name of names) try {
|
|
4316
|
+
const stat = node_fs.statSync(node_path.join(stagingRoot, name));
|
|
4317
|
+
ages.push({
|
|
4318
|
+
name,
|
|
4319
|
+
ageMs: now - stat.mtimeMs
|
|
4320
|
+
});
|
|
4321
|
+
} catch {}
|
|
4322
|
+
const sweepable = selectSweepableStaging(ages, this.inFlightStaging);
|
|
4323
|
+
if (sweepable.length > 0) this.logger.info("sweeping staging residue from a crashed earlier deploy", { meta: {
|
|
4324
|
+
stagingRoot,
|
|
4325
|
+
entries: sweepable
|
|
4326
|
+
} });
|
|
4327
|
+
await Promise.all(sweepable.map((name) => node_fs.promises.rm(node_path.join(stagingRoot, name), {
|
|
4328
|
+
recursive: true,
|
|
4329
|
+
force: true
|
|
4330
|
+
}).catch(() => void 0)));
|
|
4331
|
+
}
|
|
4187
4332
|
/** Best-effort removal of leftover `.evicting/*` dirs from earlier deploys. */
|
|
4188
4333
|
async sweepGraveyard(graveyard) {
|
|
4189
4334
|
let entries;
|
|
@@ -4201,9 +4346,11 @@ var AddonInstaller = class AddonInstaller {
|
|
|
4201
4346
|
async installFromTgz(tgzPath) {
|
|
4202
4347
|
const stagingRoot = node_path.join(this.addonsDir, ".staging");
|
|
4203
4348
|
ensureDir(stagingRoot);
|
|
4204
|
-
await this.
|
|
4205
|
-
const
|
|
4349
|
+
await this.sweepStagingResidue(stagingRoot);
|
|
4350
|
+
const stagingName = `install-${process.pid}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
|
|
4351
|
+
const stagingDir = node_path.join(stagingRoot, stagingName);
|
|
4206
4352
|
ensureDir(stagingDir);
|
|
4353
|
+
this.inFlightStaging.add(stagingName);
|
|
4207
4354
|
try {
|
|
4208
4355
|
await execFileAsync("tar", [
|
|
4209
4356
|
"-xzf",
|
|
@@ -4258,6 +4405,7 @@ var AddonInstaller = class AddonInstaller {
|
|
|
4258
4405
|
version: pkgView.version
|
|
4259
4406
|
};
|
|
4260
4407
|
} finally {
|
|
4408
|
+
this.inFlightStaging.delete(stagingName);
|
|
4261
4409
|
await node_fs.promises.rm(stagingDir, {
|
|
4262
4410
|
recursive: true,
|
|
4263
4411
|
force: true
|
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 {
|
|
2
|
+
import { $ as procedureAuthKey, Ct as asJsonObject$1, Dt as emitDownForOwnedCaps, Et as createEvent, Ft as readinessKey, H as kebabToCamel, It as resolveCapMount, K as logLevelAtMost, L as isArrayOutputSchema, Lt as scopeKey, Nt as parseJsonObject, P as extractNestedAddonId, Pt as parseJsonUnknown$1, R as isCollectionArrayMethod, Tt as asString$1, U as lifecycleJobSchema, V as isVoidInput, X as objectInputDeclaresAddonId, bt as ReadinessRegistry, c as METHOD_ACCESS_MAP, ft as errMsg$1, gt as DEVICE_STATUS_METHOD, h as addonSettingsCapability, ht as DEVICE_SETTINGS_CONTRIBUTION_METHODS, kt as expandCapMethods, l as RUNTIME_DEFAULTS, mt as DATAPLANE_SECRET_HEADER$1, q as looseSchema, t as ALL_CAPABILITY_DEFINITIONS, wt as asNumber, xt as ReadinessTimeoutError, z as isObjectInput, zt as EventCategory$1 } from "./dist-BqJJWCS8.mjs";
|
|
3
3
|
import { a as downloadModel, c as getModelFilePath, d as contentTypeFor, f as createAuthenticatedFileServer, h as resolveFilePath, i as downloadFile, l as isModelDownloaded, m as parseTokenizedUrl, n as collectModelFiles, o as ensureModel, p as parseRangeHeader, r as deleteModelFromDisk, s as fetchJson, t as ModelDownloadService, u as createFileDataPlaneHandler } from "./model-download-service-D8B-4ktF.mjs";
|
|
4
4
|
import { $ as buildNativeCapProxy, A as createHubCapForwardService, B as AGENT_CAP_FWD_ACTION, C as brokerTransportLink, Ct as resolveAddonClass, D as localProviderLink, E as ipcParentLink, F as createUdsLogger, G as callWithServiceDiscovery, H as CapRouteResolver, I as createUdsLoggerWithControl, J as UdsLocalTransportServer, K as createLocalTransport, L as LocalChildClient, M as createUdsEventBridge, N as createUdsEventBus, O as HUB_CAP_FWD_ACTION, P as udsChildLogToWorkerEntry, Q as encodeFrame, R as LocalChildRegistry, S as brokerCallForCap, St as runNpm, T as ipcChildLink, U as CapRouteError, V as AGENT_CAP_FWD_SERVICE, W as classifyCapRoute, X as localEndpointPath, Y as SocketChannel, Z as FrameDecoder, _ as createKernelHwAccel, _t as CapabilityHandle, a as getWorkerDeviceRegistry, b as __resetCapUsageRegistryForTests, bt as installManifestNativeDeps, c as setHubConnected, ct as NATIVE_PROVIDER_SERVICE_INFIX, d as getBrokerEventBus, dt as capBareAction, et as buildUdsNativeCapProxy, f as getMoleculerEventStats, ft as capServiceName, g as AddonDepsManager, gt as DeviceRegistry, h as subscribePassthrough, ht as serializeTypedArrays, i as createUdsAddonContext, it as mountNativeCapService, j as createParentUnownedCallHandler, k as HUB_CAP_FWD_SERVICE, l as EVENT_TOPIC_PREFIX, lt as capActionName, m as setNodeEventInterest, mt as deserializeTypedArrays, n as adaptBrokerToCluster, o as getOrInitReadinessRegistry, ot as createAddonService, p as registerEventBusService, pt as parseCapAction, q as UdsLocalTransportClient, r as createAddonContext, s as getOrInitReadinessRegistryForClient, st as validateProviderRegistrations, t as installManifestPythonDeps, tt as createBrokerDeviceManagerApi, u as clusterEventTopic, ut as capActionSuffix, v as resolveHwAccel, vt as CapabilityUnavailableError, w as buildLinkChain, wt as createAddonDataPlaneFacility, x as getCapUsageRegistry, xt as resolveNpmInvocation, y as CapUsageRegistry, yt as copyBundledNativeModules, z as UDS_NO_ROUTE_PREFIX } from "./manifest-python-deps-CX5tGRqf.mjs";
|
|
5
5
|
import { n as getSinglePidStats, t as getPidStats } from "./resource-monitor-BkP504Vq.mjs";
|
|
@@ -3223,6 +3223,21 @@ async function installPackageFromNpm(packageName, targetDir) {
|
|
|
3223
3223
|
//#region src/kernel/addon-manifest.ts
|
|
3224
3224
|
var MANIFEST_FILENAME = "manifest.json";
|
|
3225
3225
|
var MANIFEST_VERSION = 1;
|
|
3226
|
+
/**
|
|
3227
|
+
* Is `p` a directory, FOLLOWING symlinks?
|
|
3228
|
+
*
|
|
3229
|
+
* `Dirent.isDirectory()` answers false for a symlink, and dev installs
|
|
3230
|
+
* (`installSymlink`) point `addonsDir/@camstack/<pkg>` straight at the
|
|
3231
|
+
* workspace source. Trusting the dirent there finds zero addons on disk, which
|
|
3232
|
+
* with pruning would empty a developer's manifest on the next boot.
|
|
3233
|
+
*/
|
|
3234
|
+
function isDirLike(p) {
|
|
3235
|
+
try {
|
|
3236
|
+
return fs$17.statSync(p).isDirectory();
|
|
3237
|
+
} catch {
|
|
3238
|
+
return false;
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3226
3241
|
var AddonManifest = class {
|
|
3227
3242
|
manifestPath;
|
|
3228
3243
|
cache = null;
|
|
@@ -3312,35 +3327,48 @@ var AddonManifest = class {
|
|
|
3312
3327
|
this.cache = null;
|
|
3313
3328
|
}
|
|
3314
3329
|
/**
|
|
3315
|
-
* Reconcile the manifest
|
|
3330
|
+
* Reconcile the manifest against what is actually present on disk.
|
|
3316
3331
|
*
|
|
3317
|
-
*
|
|
3318
|
-
*
|
|
3319
|
-
*
|
|
3320
|
-
* version and timestamps are authoritative — a later install/update
|
|
3321
|
-
* overwrites them through `upsert`).
|
|
3332
|
+
* **The directory is the truth.** It is what `AddonLoader` runs; a manifest
|
|
3333
|
+
* that disagrees with it is a lie the update flow and the admin UI then
|
|
3334
|
+
* repeat. Three corrections, all idempotent and safe on every boot:
|
|
3322
3335
|
*
|
|
3323
|
-
*
|
|
3324
|
-
*
|
|
3325
|
-
*
|
|
3326
|
-
* present and skips, never calling `upsert`)
|
|
3327
|
-
*
|
|
3328
|
-
*
|
|
3329
|
-
*
|
|
3330
|
-
*
|
|
3336
|
+
* - **added** — an on-disk addon with no entry gets one, source `'seed'`.
|
|
3337
|
+
* Addons baked into the Docker/Electron image are copied straight into
|
|
3338
|
+
* `addonsDir` by the container entrypoint (`ensureRequiredPackages` then
|
|
3339
|
+
* sees `package.json` present and skips, never calling `upsert`), so they
|
|
3340
|
+
* would otherwise stay untracked and `applyUpdate` would reject them with
|
|
3341
|
+
* "not currently tracked in manifest" — what broke "Update all" on a
|
|
3342
|
+
* fresh image.
|
|
3343
|
+
* - **version-corrected** — an entry whose version disagrees with the
|
|
3344
|
+
* installed `package.json` adopts the DISK version. Provenance (`source`,
|
|
3345
|
+
* `installedAt`) is NOT derivable from disk and stays authoritative here;
|
|
3346
|
+
* `updatedAt` is left alone because the correction is not an update, and
|
|
3347
|
+
* stamping `now` would claim an install that never happened.
|
|
3348
|
+
* - **pruned** — an entry naming a directory that does not exist. The live
|
|
3349
|
+
* hub carried three (`@camstack/system 1.2.29`, `addon-viewer-ui 1.1.39`,
|
|
3350
|
+
* `addon-agent-ui 1.2.4`); `@camstack/system` in particular is
|
|
3351
|
+
* host-provided and deliberately NOT seeded under `addonsDir`
|
|
3352
|
+
* ([D45]), so its entry advertised a copy that must not exist.
|
|
3353
|
+
* An entry holding a `lastBackupDir` is NEVER pruned: that pointer is the
|
|
3354
|
+
* only reference to a pre-update backup, and the launcher's backup sweep
|
|
3355
|
+
* reclaims anything the manifest does not point at.
|
|
3331
3356
|
*
|
|
3332
|
-
*
|
|
3357
|
+
* `onChange` receives one record per correction, so the caller can log
|
|
3358
|
+
* exactly what moved. Returns the total number of changes.
|
|
3333
3359
|
*/
|
|
3334
|
-
reconcileFromDisk(addonsDir) {
|
|
3360
|
+
reconcileFromDisk(addonsDir, onChange) {
|
|
3335
3361
|
if (!fs$17.existsSync(addonsDir)) return 0;
|
|
3336
3362
|
const m = this.read();
|
|
3337
|
-
let
|
|
3363
|
+
let changes = 0;
|
|
3338
3364
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3365
|
+
/** Package name → version, for every ADDON package actually installed. */
|
|
3366
|
+
const onDisk = /* @__PURE__ */ new Map();
|
|
3339
3367
|
for (const scope of fs$17.readdirSync(addonsDir, { withFileTypes: true })) {
|
|
3340
|
-
if (!scope.
|
|
3368
|
+
if (!scope.name.startsWith("@") || !isDirLike(path$39.join(addonsDir, scope.name))) continue;
|
|
3341
3369
|
const scopeDir = path$39.join(addonsDir, scope.name);
|
|
3342
3370
|
for (const pkg of fs$17.readdirSync(scopeDir, { withFileTypes: true })) {
|
|
3343
|
-
if (!pkg.
|
|
3371
|
+
if (!isDirLike(path$39.join(scopeDir, pkg.name))) continue;
|
|
3344
3372
|
const pkgJsonPath = path$39.join(scopeDir, pkg.name, "package.json");
|
|
3345
3373
|
if (!fs$17.existsSync(pkgJsonPath)) continue;
|
|
3346
3374
|
let pkgJson;
|
|
@@ -3354,22 +3382,57 @@ var AddonManifest = class {
|
|
|
3354
3382
|
if (!pkgJson.name || !pkgJson.version) continue;
|
|
3355
3383
|
const addons = pkgJson.camstack?.addons;
|
|
3356
3384
|
if (!Array.isArray(addons) || addons.length === 0) continue;
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3385
|
+
onDisk.set(pkgJson.name, pkgJson.version);
|
|
3386
|
+
}
|
|
3387
|
+
}
|
|
3388
|
+
for (const [name, version] of onDisk) {
|
|
3389
|
+
const prev = m.addons[name];
|
|
3390
|
+
if (prev == null) {
|
|
3391
|
+
m.addons[name] = {
|
|
3392
|
+
name,
|
|
3393
|
+
version,
|
|
3361
3394
|
source: "seed",
|
|
3362
3395
|
installedAt: now,
|
|
3363
3396
|
updatedAt: now
|
|
3364
3397
|
};
|
|
3365
|
-
|
|
3398
|
+
changes++;
|
|
3399
|
+
onChange?.({
|
|
3400
|
+
kind: "added",
|
|
3401
|
+
name,
|
|
3402
|
+
to: version
|
|
3403
|
+
});
|
|
3404
|
+
continue;
|
|
3366
3405
|
}
|
|
3406
|
+
if (prev.version !== version) {
|
|
3407
|
+
m.addons[name] = {
|
|
3408
|
+
...prev,
|
|
3409
|
+
version
|
|
3410
|
+
};
|
|
3411
|
+
changes++;
|
|
3412
|
+
onChange?.({
|
|
3413
|
+
kind: "version-corrected",
|
|
3414
|
+
name,
|
|
3415
|
+
from: prev.version,
|
|
3416
|
+
to: version
|
|
3417
|
+
});
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
for (const [name, entry] of Object.entries(m.addons)) {
|
|
3421
|
+
if (onDisk.has(name)) continue;
|
|
3422
|
+
if (entry.lastBackupDir) continue;
|
|
3423
|
+
delete m.addons[name];
|
|
3424
|
+
changes++;
|
|
3425
|
+
onChange?.({
|
|
3426
|
+
kind: "pruned",
|
|
3427
|
+
name,
|
|
3428
|
+
from: entry.version
|
|
3429
|
+
});
|
|
3367
3430
|
}
|
|
3368
|
-
if (
|
|
3431
|
+
if (changes > 0) {
|
|
3369
3432
|
m.updatedAt = now;
|
|
3370
3433
|
this.write(m);
|
|
3371
3434
|
}
|
|
3372
|
-
return
|
|
3435
|
+
return changes;
|
|
3373
3436
|
}
|
|
3374
3437
|
/**
|
|
3375
3438
|
* Migration helper: if the manifest is empty but the addons directory
|
|
@@ -3752,6 +3815,34 @@ var noopLogger = {
|
|
|
3752
3815
|
return noopLogger;
|
|
3753
3816
|
}
|
|
3754
3817
|
};
|
|
3818
|
+
/**
|
|
3819
|
+
* A staging tree younger than this may belong to an install still running —
|
|
3820
|
+
* possibly in another process. Only older trees are residue worth sweeping.
|
|
3821
|
+
* Generous on purpose: an addon's runtime-dependency `npm install` on a slow
|
|
3822
|
+
* host is minutes, and the cost of leaving residue one deploy longer is disk,
|
|
3823
|
+
* while the cost of sweeping a live one is a FAILED deploy.
|
|
3824
|
+
*/
|
|
3825
|
+
var STAGING_RESIDUE_MIN_AGE_MS = 30 * 6e4;
|
|
3826
|
+
/**
|
|
3827
|
+
* Pick the staging entries that are safe to delete.
|
|
3828
|
+
*
|
|
3829
|
+
* `installFromTgz` used to clear the WHOLE `.staging` root before creating its
|
|
3830
|
+
* own dir. Two deploys arriving within one install's lifetime — the normal case
|
|
3831
|
+
* when the hub propagates two packages back to back — meant the second one
|
|
3832
|
+
* deleted the first one's tree out from under its running `npm install`:
|
|
3833
|
+
*
|
|
3834
|
+
* npm error code ENOENT
|
|
3835
|
+
* npm error path …/.staging/install-14820-2026-08-07T12-28-28-875Z/package/package.json
|
|
3836
|
+
*
|
|
3837
|
+
* which the propagation then reported as `runtime dependency install failed`.
|
|
3838
|
+
* Observed on the Mac desktop agent and on little-unraid on 2026-08-07.
|
|
3839
|
+
*
|
|
3840
|
+
* So a sweep only removes what cannot be live: not a tree this process is
|
|
3841
|
+
* building, and not one young enough to belong to anybody's in-flight install.
|
|
3842
|
+
*/
|
|
3843
|
+
function selectSweepableStaging(entries, inFlight, minAgeMs = STAGING_RESIDUE_MIN_AGE_MS) {
|
|
3844
|
+
return entries.filter((entry) => !inFlight.has(entry.name) && entry.ageMs >= minAgeMs).map((entry) => entry.name);
|
|
3845
|
+
}
|
|
3755
3846
|
var AddonInstaller = class AddonInstaller {
|
|
3756
3847
|
addonsDir;
|
|
3757
3848
|
registry;
|
|
@@ -3768,6 +3859,11 @@ var AddonInstaller = class AddonInstaller {
|
|
|
3768
3859
|
* `getAddonInstallSource`).
|
|
3769
3860
|
*/
|
|
3770
3861
|
manifest;
|
|
3862
|
+
/**
|
|
3863
|
+
* Basenames under `.staging` this process is currently building. Never swept
|
|
3864
|
+
* — see {@link selectSweepableStaging}.
|
|
3865
|
+
*/
|
|
3866
|
+
inFlightStaging = /* @__PURE__ */ new Set();
|
|
3771
3867
|
constructor(config, logger = noopLogger) {
|
|
3772
3868
|
this.addonsDir = config.addonsDir;
|
|
3773
3869
|
this.registry = config.registry;
|
|
@@ -3926,19 +4022,34 @@ var AddonInstaller = class AddonInstaller {
|
|
|
3926
4022
|
}
|
|
3927
4023
|
}
|
|
3928
4024
|
/**
|
|
3929
|
-
* Reconcile the install manifest
|
|
3930
|
-
* disk
|
|
3931
|
-
*
|
|
3932
|
-
*
|
|
3933
|
-
*
|
|
3934
|
-
*
|
|
4025
|
+
* Reconcile the install manifest against the addons actually present on
|
|
4026
|
+
* disk: register seeded addons, correct a version that disagrees with the
|
|
4027
|
+
* installed `package.json`, and drop entries naming a directory that does
|
|
4028
|
+
* not exist. Call once after `ensureRequiredPackages`, before any update
|
|
4029
|
+
* can be in flight. See `AddonManifest.reconcileFromDisk` for the rules.
|
|
4030
|
+
*
|
|
4031
|
+
* Every correction is logged by name — a manifest that silently disagreed
|
|
4032
|
+
* with disk is what made "@camstack/system 1.2.29" read as installed on a
|
|
4033
|
+
* hub that deliberately has no copy of it.
|
|
3935
4034
|
*
|
|
3936
|
-
* Returns the number of
|
|
4035
|
+
* Returns the number of changes.
|
|
3937
4036
|
*/
|
|
3938
4037
|
reconcileManifest() {
|
|
3939
|
-
const
|
|
3940
|
-
|
|
3941
|
-
|
|
4038
|
+
const changes = this.manifest.reconcileFromDisk(this.addonsDir, (c) => {
|
|
4039
|
+
switch (c.kind) {
|
|
4040
|
+
case "added":
|
|
4041
|
+
this.logger.info(`Manifest — registered seeded addon ${c.name}@${c.to}`);
|
|
4042
|
+
break;
|
|
4043
|
+
case "version-corrected":
|
|
4044
|
+
this.logger.info(`Manifest — ${c.name} recorded ${c.from} but disk has ${c.to}; adopting disk`);
|
|
4045
|
+
break;
|
|
4046
|
+
case "pruned":
|
|
4047
|
+
this.logger.info(`Manifest — dropped ${c.name}@${c.from}: no install directory on disk`);
|
|
4048
|
+
break;
|
|
4049
|
+
}
|
|
4050
|
+
});
|
|
4051
|
+
if (changes > 0) this.logger.info(`Manifest reconciled — ${changes} correction(s)`);
|
|
4052
|
+
return changes;
|
|
3942
4053
|
}
|
|
3943
4054
|
/**
|
|
3944
4055
|
* Install a single addon — on-demand installer used by AddonPackageService
|
|
@@ -4176,6 +4287,40 @@ var AddonInstaller = class AddonInstaller {
|
|
|
4176
4287
|
} });
|
|
4177
4288
|
});
|
|
4178
4289
|
}
|
|
4290
|
+
/**
|
|
4291
|
+
* Best-effort removal of `.staging` residue left by a CRASHED earlier deploy.
|
|
4292
|
+
*
|
|
4293
|
+
* Deliberately not `sweepGraveyard`: an evicted `.evicting/*` dir is dead by
|
|
4294
|
+
* construction, but a `.staging/*` dir may be an install still running — in
|
|
4295
|
+
* this process or another one. Deleting one of those fails a live deploy with
|
|
4296
|
+
* an ENOENT on its own package.json, which is what happened on 2026-08-07.
|
|
4297
|
+
*/
|
|
4298
|
+
async sweepStagingResidue(stagingRoot) {
|
|
4299
|
+
let names;
|
|
4300
|
+
try {
|
|
4301
|
+
names = fs$17.readdirSync(stagingRoot);
|
|
4302
|
+
} catch {
|
|
4303
|
+
return;
|
|
4304
|
+
}
|
|
4305
|
+
const now = Date.now();
|
|
4306
|
+
const ages = [];
|
|
4307
|
+
for (const name of names) try {
|
|
4308
|
+
const stat = fs$17.statSync(path$39.join(stagingRoot, name));
|
|
4309
|
+
ages.push({
|
|
4310
|
+
name,
|
|
4311
|
+
ageMs: now - stat.mtimeMs
|
|
4312
|
+
});
|
|
4313
|
+
} catch {}
|
|
4314
|
+
const sweepable = selectSweepableStaging(ages, this.inFlightStaging);
|
|
4315
|
+
if (sweepable.length > 0) this.logger.info("sweeping staging residue from a crashed earlier deploy", { meta: {
|
|
4316
|
+
stagingRoot,
|
|
4317
|
+
entries: sweepable
|
|
4318
|
+
} });
|
|
4319
|
+
await Promise.all(sweepable.map((name) => fs$17.promises.rm(path$39.join(stagingRoot, name), {
|
|
4320
|
+
recursive: true,
|
|
4321
|
+
force: true
|
|
4322
|
+
}).catch(() => void 0)));
|
|
4323
|
+
}
|
|
4179
4324
|
/** Best-effort removal of leftover `.evicting/*` dirs from earlier deploys. */
|
|
4180
4325
|
async sweepGraveyard(graveyard) {
|
|
4181
4326
|
let entries;
|
|
@@ -4193,9 +4338,11 @@ var AddonInstaller = class AddonInstaller {
|
|
|
4193
4338
|
async installFromTgz(tgzPath) {
|
|
4194
4339
|
const stagingRoot = path$39.join(this.addonsDir, ".staging");
|
|
4195
4340
|
ensureDir(stagingRoot);
|
|
4196
|
-
await this.
|
|
4197
|
-
const
|
|
4341
|
+
await this.sweepStagingResidue(stagingRoot);
|
|
4342
|
+
const stagingName = `install-${process.pid}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
|
|
4343
|
+
const stagingDir = path$39.join(stagingRoot, stagingName);
|
|
4198
4344
|
ensureDir(stagingDir);
|
|
4345
|
+
this.inFlightStaging.add(stagingName);
|
|
4199
4346
|
try {
|
|
4200
4347
|
await execFileAsync("tar", [
|
|
4201
4348
|
"-xzf",
|
|
@@ -4250,6 +4397,7 @@ var AddonInstaller = class AddonInstaller {
|
|
|
4250
4397
|
version: pkgView.version
|
|
4251
4398
|
};
|
|
4252
4399
|
} finally {
|
|
4400
|
+
this.inFlightStaging.delete(stagingName);
|
|
4253
4401
|
await fs$17.promises.rm(stagingDir, {
|
|
4254
4402
|
recursive: true,
|
|
4255
4403
|
force: true
|