@camstack/system 1.2.57 → 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.
Files changed (69) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/addon-utils.js +1 -1
  4. package/dist/addon-utils.mjs +1 -1
  5. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +2 -2
  6. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +2 -2
  7. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +2 -2
  8. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +2 -2
  9. package/dist/builtins/alerts/alerts.addon.js +1 -1
  10. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  11. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
  13. package/dist/builtins/console-logging/index.js +1 -1
  14. package/dist/builtins/console-logging/index.mjs +1 -1
  15. package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
  16. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  17. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  18. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  19. package/dist/builtins/doorbell/binding-mirror.d.ts +65 -0
  20. package/dist/builtins/doorbell/virtual-doorbell.addon.d.ts +56 -11
  21. package/dist/builtins/doorbell/virtual-doorbell.addon.js +248 -26
  22. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +248 -26
  23. package/dist/builtins/hub-forwarder/index.js +1 -1
  24. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  25. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  26. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  27. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  28. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  29. package/dist/builtins/local-network/local-network.addon.js +1 -1
  30. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  31. package/dist/builtins/loki-logging/index.js +1 -1
  32. package/dist/builtins/loki-logging/index.mjs +1 -1
  33. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  34. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  35. package/dist/builtins/platform-probe/index.js +1 -1
  36. package/dist/builtins/platform-probe/index.mjs +1 -1
  37. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  38. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  39. package/dist/builtins/snapshot/index.js +2 -2
  40. package/dist/builtins/snapshot/index.mjs +2 -2
  41. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +2 -2
  42. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +2 -2
  43. package/dist/builtins/sqlite-storage/index.d.ts +3 -0
  44. package/dist/builtins/sqlite-storage/index.js +8 -0
  45. package/dist/builtins/sqlite-storage/index.mjs +2 -2
  46. package/dist/builtins/sqlite-storage/retired-settings-keys.d.ts +94 -0
  47. package/dist/builtins/sqlite-storage/sqlite-pragmas.d.ts +85 -0
  48. package/dist/builtins/sqlite-storage/sqlite-settings.addon.d.ts +1 -0
  49. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +445 -2
  50. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +438 -3
  51. package/dist/builtins/sqlite-storage/wal-maintenance.d.ts +113 -0
  52. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  53. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  54. package/dist/builtins/system-config/system-config.addon.js +1 -1
  55. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  56. package/dist/builtins/winston-logging/index.js +1 -1
  57. package/dist/builtins/winston-logging/index.mjs +1 -1
  58. package/dist/{dist-DhL_zGRT.mjs → dist-BqJJWCS8.mjs} +994 -34
  59. package/dist/{dist-BupxnZOi.js → dist-CV6XTApk.js} +1005 -33
  60. package/dist/index.js +190 -42
  61. package/dist/index.mjs +191 -43
  62. package/dist/kernel/addon-installer.d.ts +55 -7
  63. package/dist/kernel/addon-manifest.d.ts +36 -16
  64. package/dist/kernel/transport/frame-codec.d.ts +43 -2
  65. package/dist/{manifest-python-deps-Ck4-9K9m.mjs → manifest-python-deps-CX5tGRqf.mjs} +95 -10
  66. package/dist/{manifest-python-deps-BqE5j0-O.js → manifest-python-deps-d4acYXVV.js} +95 -10
  67. package/package.json +1 -1
  68. package/dist/{model-download-service-hf0ookyy.js → model-download-service-Cpc6SWzV.js} +1 -1
  69. package/dist/{model-download-service-Cp9f4dk6.mjs → model-download-service-D8B-4ktF.mjs} +1 -1
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("./chunk-Cek0wNdY.js");
3
- const require_dist = require("./dist-BupxnZOi.js");
4
- const require_model_download_service = require("./model-download-service-hf0ookyy.js");
5
- const require_manifest_python_deps = require("./manifest-python-deps-BqE5j0-O.js");
3
+ const require_dist = require("./dist-CV6XTApk.js");
4
+ const require_model_download_service = require("./model-download-service-Cpc6SWzV.js");
5
+ const require_manifest_python_deps = require("./manifest-python-deps-d4acYXVV.js");
6
6
  const require_resource_monitor = require("./resource-monitor-DNNomR-i.js");
7
7
  const require_builtins_sqlite_storage_filesystem_storage_addon = require("./builtins/sqlite-storage/filesystem-storage.addon.js");
8
8
  const require_builtins_sqlite_storage_sqlite_settings_addon = require("./builtins/sqlite-storage/sqlite-settings.addon.js");
@@ -32,13 +32,13 @@ let node_v8 = require("node:v8");
32
32
  node_v8 = require_chunk.__toESM(node_v8);
33
33
  let _camstack_types_node = require("@camstack/types/node");
34
34
  let zod = require("zod");
35
+ let node_crypto = require("node:crypto");
35
36
  let node_http = require("node:http");
36
37
  let node_fs = require("node:fs");
37
38
  let node_fs$1 = require_chunk.__toESM(node_fs, 1);
38
39
  node_fs = require_chunk.__toESM(node_fs);
39
40
  let node_path = require("node:path");
40
41
  node_path = require_chunk.__toESM(node_path, 1);
41
- let node_crypto = require("node:crypto");
42
42
  let _camstack_types_addon = require("@camstack/types/addon");
43
43
  let node_child_process = require("node:child_process");
44
44
  let node_util = require("node:util");
@@ -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 with what is actually present on disk.
3338
+ * Reconcile the manifest against what is actually present on disk.
3324
3339
  *
3325
- * For every `@scope/<pkg>` directory under `addonsDir` that has a
3326
- * readable `package.json` but NO manifest entry yet, add one with
3327
- * source `'seed'`. Existing entries are left untouched (their source,
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
- * Why this exists: addons baked into the Docker/Electron image are
3332
- * copied straight into `addonsDir` by the container entrypoint (the
3333
- * launcher's `ensureRequiredPackages` then sees `package.json` already
3334
- * present and skips, never calling `upsert`). Without this reconcile
3335
- * those seeded addons stay absent from the manifest, so `applyUpdate`
3336
- * rejects them with "not currently tracked in manifest" — which is
3337
- * exactly what broke "Update all" on a fresh image. Idempotent and
3338
- * safe to run on every boot.
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
- * Returns the number of entries added.
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 added = 0;
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.isDirectory() || !scope.name.startsWith("@")) continue;
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.isDirectory()) continue;
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
- if (m.addons[pkgJson.name] != null) continue;
3366
- m.addons[pkgJson.name] = {
3367
- name: pkgJson.name,
3368
- version: pkgJson.version,
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
- added++;
3406
+ changes++;
3407
+ onChange?.({
3408
+ kind: "added",
3409
+ name,
3410
+ to: version
3411
+ });
3412
+ continue;
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
+ });
3374
3426
  }
3375
3427
  }
3376
- if (added > 0) {
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
+ });
3438
+ }
3439
+ if (changes > 0) {
3377
3440
  m.updatedAt = now;
3378
3441
  this.write(m);
3379
3442
  }
3380
- return added;
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 with the addons actually present on
3938
- * disk. Seeded addons (baked into the image and copied verbatim by the
3939
- * container entrypoint) never pass through an `install*` codepath, so
3940
- * they are missing from `manifest.json` and `applyUpdate` would reject
3941
- * them with "not currently tracked in manifest". Call this once after
3942
- * `ensureRequiredPackages` so every on-disk addon is updatable.
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.
3943
4038
  *
3944
- * Returns the number of manifest entries added.
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.
4042
+ *
4043
+ * Returns the number of changes.
3945
4044
  */
3946
4045
  reconcileManifest() {
3947
- const added = this.manifest.reconcileFromDisk(this.addonsDir);
3948
- if (added > 0) this.logger.info(`Manifest reconciled — ${added} seeded addon(s) registered`);
3949
- return added;
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.sweepGraveyard(stagingRoot);
4205
- const stagingDir = node_path.join(stagingRoot, `install-${process.pid}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`);
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