@camstack/system 1.2.14 → 1.2.16
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/addon-runner.js +2 -2
- package/dist/addon-runner.mjs +2 -2
- 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 +73 -2
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +73 -2
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.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/local-auth/local-auth.addon.js +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +3 -38
- 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 +2 -2
- package/dist/builtins/platform-probe/index.js +2 -2
- package/dist/builtins/platform-probe/index.mjs +3 -2
- 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-backup/system-backup.service.d.ts +14 -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/chunk-CNf5ZN-e.mjs +37 -0
- package/dist/{dist-yttC1aUI.js → dist-3AzhkmeY.js} +1325 -1129
- package/dist/{dist-eOaraBhT.mjs → dist-BU-QPlE8.mjs} +1325 -1129
- package/dist/framework-resolver-hook.mjs +2 -0
- package/dist/index.js +224 -32
- package/dist/index.mjs +227 -34
- package/dist/kernel/addon-installer.d.ts +86 -7
- package/dist/kernel/fs-utils.d.ts +18 -5
- package/dist/lib-5THG9eKz.mjs +15545 -0
- package/dist/lib-CZ3g2WTS.js +15549 -0
- package/dist/{manifest-python-deps-bn3qrsxR.mjs → manifest-python-deps-D7-WBIg_.mjs} +1 -1
- package/dist/{manifest-python-deps-DSrmih6E.js → manifest-python-deps-DRN3x53E.js} +1 -1
- package/dist/{resource-monitor-Le3l0qS2.mjs → resource-monitor-BkP504Vq.mjs} +1 -1
- package/package.json +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-
|
|
3
|
+
const require_dist = require("./dist-3AzhkmeY.js");
|
|
4
4
|
const require_model_download_service = require("./model-download-service-hf0ookyy.js");
|
|
5
|
-
const require_manifest_python_deps = require("./manifest-python-deps-
|
|
5
|
+
const require_manifest_python_deps = require("./manifest-python-deps-DRN3x53E.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");
|
|
@@ -2733,21 +2733,26 @@ async function copyDirRecursive(src, dest) {
|
|
|
2733
2733
|
});
|
|
2734
2734
|
}
|
|
2735
2735
|
/**
|
|
2736
|
-
* Runtime
|
|
2737
|
-
*
|
|
2738
|
-
*
|
|
2739
|
-
*
|
|
2740
|
-
*
|
|
2736
|
+
* Runtime deps a stripped addon package.json must KEEP so the installer still
|
|
2737
|
+
* `npm install`s them into the addon dir.
|
|
2738
|
+
*
|
|
2739
|
+
* Empty by design: every native an addon can import is now HOST-EXTERNAL
|
|
2740
|
+
* (node-av, sharp, node-pty — resolved from the framework closure via the
|
|
2741
|
+
* resolver hook, see host-externals.ts), and every pure-JS lib (ssh2, werift,
|
|
2742
|
+
* @simplewebauthn, mqtt, aws-sdk, …) is inlined by the self-contained vite
|
|
2743
|
+
* bundle. So a self-contained dist requires NOTHING from a local node_modules —
|
|
2744
|
+
* `addons/` stays node_modules-free, which is what lets the backup tar the whole
|
|
2745
|
+
* folder with no exclusion list (see ADR-0027). An addon that genuinely needs an
|
|
2746
|
+
* un-bundleable runtime dep declares it explicitly in
|
|
2747
|
+
* `camstack.runtimeDependencies`; the debug escape hatch
|
|
2748
|
+
* `CAMSTACK_INSTALL_FULL_DEPS=1` restores the legacy install-everything path.
|
|
2749
|
+
*
|
|
2750
|
+
* (Historically this mirrored the build preset's COMMON_NATIVE_DEPS and
|
|
2751
|
+
* per-addon-installed node-av/sharp/onnxruntime-node/ssh2/werift/systeminformation
|
|
2752
|
+
* — 74k files / 1.34 GB of redundant node_modules on the live hub. That redundancy
|
|
2753
|
+
* is removed: the natives come from the framework, the pure-JS from the bundle.)
|
|
2741
2754
|
*/
|
|
2742
|
-
var RUNTIME_NATIVE_KEEP = [
|
|
2743
|
-
"better-sqlite3",
|
|
2744
|
-
"node-av",
|
|
2745
|
-
"onnxruntime-node",
|
|
2746
|
-
"sharp",
|
|
2747
|
-
"ssh2",
|
|
2748
|
-
"werift",
|
|
2749
|
-
"systeminformation"
|
|
2750
|
-
];
|
|
2755
|
+
var RUNTIME_NATIVE_KEEP = [];
|
|
2751
2756
|
/** Read `camstack.runtimeDependencies` (extra non-native deps the dist
|
|
2752
2757
|
* requires at runtime — versions come from the manifest's own dep maps). */
|
|
2753
2758
|
function readRuntimeDependencies(pkg) {
|
|
@@ -3934,10 +3939,14 @@ var AddonInstaller = class AddonInstaller {
|
|
|
3934
3939
|
* 1. Rename current install dir → {addonsDir}/.backups/{name}/{ts}/
|
|
3935
3940
|
* 2. Run a fresh install (npm or local depending on installSource)
|
|
3936
3941
|
* 3. Record `lastBackupDir` in the manifest entry
|
|
3942
|
+
* 4. Prune every OTHER backup of this addon — the previous one just
|
|
3943
|
+
* lost its only pointer and would otherwise be orphaned forever.
|
|
3937
3944
|
*
|
|
3938
|
-
* The post-install caller (
|
|
3939
|
-
*
|
|
3940
|
-
*
|
|
3945
|
+
* The post-install caller (restart + health-check) is responsible for
|
|
3946
|
+
* verifying the new version actually works, and for calling either
|
|
3947
|
+
* `clearBackup()` on success or `rollbackAddon()` on failure. This
|
|
3948
|
+
* method must NOT delete the backup itself: at this point the new
|
|
3949
|
+
* version has not been proven to work.
|
|
3941
3950
|
*
|
|
3942
3951
|
* Throws if there is no installed version to update — caller should
|
|
3943
3952
|
* prefer plain `install()` for first-time installs.
|
|
@@ -3990,6 +3999,7 @@ var AddonInstaller = class AddonInstaller {
|
|
|
3990
3999
|
source: this.manifest.get(packageName)?.source ?? "npm",
|
|
3991
4000
|
lastBackupDir: backupDir
|
|
3992
4001
|
});
|
|
4002
|
+
await this.pruneBackups(packageName, backupDir);
|
|
3993
4003
|
return {
|
|
3994
4004
|
name: result.name,
|
|
3995
4005
|
version: result.version,
|
|
@@ -4060,6 +4070,7 @@ var AddonInstaller = class AddonInstaller {
|
|
|
4060
4070
|
source: this.manifest.get(packageName)?.source ?? "npm",
|
|
4061
4071
|
lastBackupDir: backupDir
|
|
4062
4072
|
});
|
|
4073
|
+
await this.pruneBackups(packageName, backupDir);
|
|
4063
4074
|
return {
|
|
4064
4075
|
name: packageName,
|
|
4065
4076
|
version,
|
|
@@ -4106,23 +4117,204 @@ var AddonInstaller = class AddonInstaller {
|
|
|
4106
4117
|
this.logger.info(`${packageName} — rolled back to v${restoredVersion}`);
|
|
4107
4118
|
return restoredVersion;
|
|
4108
4119
|
}
|
|
4120
|
+
/** Root of the pre-update rollback copies: `{addonsDir}/.backups`. */
|
|
4121
|
+
get backupsRoot() {
|
|
4122
|
+
return node_path.join(this.addonsDir, ".backups");
|
|
4123
|
+
}
|
|
4124
|
+
/**
|
|
4125
|
+
* True only when `target` resolves to something strictly INSIDE
|
|
4126
|
+
* `{addonsDir}/.backups/`.
|
|
4127
|
+
*
|
|
4128
|
+
* Every deletion in this section funnels through here. `lastBackupDir`
|
|
4129
|
+
* is read back from a manifest file on disk — an operator edit, a
|
|
4130
|
+
* restored manifest from another host, or a `..` in the recorded path
|
|
4131
|
+
* must never turn "reclaim a backup" into "delete the live install".
|
|
4132
|
+
*/
|
|
4133
|
+
isInsideBackups(target) {
|
|
4134
|
+
const rel = node_path.relative(node_path.resolve(this.backupsRoot), node_path.resolve(target));
|
|
4135
|
+
return rel !== "" && !rel.startsWith("..") && !node_path.isAbsolute(rel);
|
|
4136
|
+
}
|
|
4109
4137
|
/**
|
|
4110
|
-
*
|
|
4111
|
-
*
|
|
4112
|
-
*
|
|
4138
|
+
* Best-effort removal of one path under `.backups/`.
|
|
4139
|
+
*
|
|
4140
|
+
* Never throws: an undeleted backup is a disk-space problem, whereas a
|
|
4141
|
+
* throw on the post-update success path would turn a working update into
|
|
4142
|
+
* a reported outage. Returns true only when the path is gone.
|
|
4143
|
+
*/
|
|
4144
|
+
async removeBackupPath(target, reason) {
|
|
4145
|
+
if (!this.isInsideBackups(target)) {
|
|
4146
|
+
this.logger.error("Refusing to remove a backup path outside .backups/", { meta: {
|
|
4147
|
+
target,
|
|
4148
|
+
backupsRoot: this.backupsRoot,
|
|
4149
|
+
reason
|
|
4150
|
+
} });
|
|
4151
|
+
return false;
|
|
4152
|
+
}
|
|
4153
|
+
try {
|
|
4154
|
+
await node_fs.promises.rm(target, {
|
|
4155
|
+
recursive: true,
|
|
4156
|
+
force: true
|
|
4157
|
+
});
|
|
4158
|
+
return true;
|
|
4159
|
+
} catch (err) {
|
|
4160
|
+
this.logger.warn("Backup removal failed; leaving residue for the next boot sweep", { meta: {
|
|
4161
|
+
target,
|
|
4162
|
+
reason,
|
|
4163
|
+
error: require_dist.errMsg(err)
|
|
4164
|
+
} });
|
|
4165
|
+
return false;
|
|
4166
|
+
}
|
|
4167
|
+
}
|
|
4168
|
+
/** List the timestamped backup dirs recorded for one package. */
|
|
4169
|
+
listBackupsFor(packageName) {
|
|
4170
|
+
const dir = node_path.join(this.backupsRoot, packageName);
|
|
4171
|
+
try {
|
|
4172
|
+
return node_fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => node_path.join(dir, e.name)).sort();
|
|
4173
|
+
} catch {
|
|
4174
|
+
return [];
|
|
4175
|
+
}
|
|
4176
|
+
}
|
|
4177
|
+
/**
|
|
4178
|
+
* Keep at most ONE backup per addon: drop every backup dir for
|
|
4179
|
+
* `packageName` except `keepDir`.
|
|
4180
|
+
*
|
|
4181
|
+
* Called right after a new backup is recorded. Without this, a second
|
|
4182
|
+
* update overwrites `lastBackupDir` and the previous backup becomes
|
|
4183
|
+
* unreachable — no pointer, so neither `rollbackAddon` nor `clearBackup`
|
|
4184
|
+
* can ever see it again. That is how the live hub accumulated two 398 MiB
|
|
4185
|
+
* copies of one addon. Never throws.
|
|
4186
|
+
*/
|
|
4187
|
+
async pruneBackups(packageName, keepDir) {
|
|
4188
|
+
const keep = keepDir == null ? null : node_path.resolve(keepDir);
|
|
4189
|
+
const removed = [];
|
|
4190
|
+
for (const candidate of this.listBackupsFor(packageName)) {
|
|
4191
|
+
if (keep != null && node_path.resolve(candidate) === keep) continue;
|
|
4192
|
+
if (await this.removeBackupPath(candidate, "superseded-backup")) removed.push(candidate);
|
|
4193
|
+
}
|
|
4194
|
+
if (removed.length > 0) this.logger.info(`${packageName} — pruned ${removed.length} superseded backup(s)`);
|
|
4195
|
+
await this.removeEmptyBackupDirs(packageName);
|
|
4196
|
+
return removed;
|
|
4197
|
+
}
|
|
4198
|
+
/** Drop `.backups/{pkg}` (and its `@scope` parent) once they are empty. */
|
|
4199
|
+
async removeEmptyBackupDirs(packageName) {
|
|
4200
|
+
const pkgDir = node_path.join(this.backupsRoot, packageName);
|
|
4201
|
+
for (const dir of [pkgDir, node_path.dirname(pkgDir)]) {
|
|
4202
|
+
if (!this.isInsideBackups(dir)) continue;
|
|
4203
|
+
try {
|
|
4204
|
+
await node_fs.promises.rmdir(dir);
|
|
4205
|
+
} catch {
|
|
4206
|
+
return;
|
|
4207
|
+
}
|
|
4208
|
+
}
|
|
4209
|
+
}
|
|
4210
|
+
/**
|
|
4211
|
+
* Drop the recorded backup for an addon — call once the post-update
|
|
4212
|
+
* health check confirms the new version actually works.
|
|
4213
|
+
*
|
|
4214
|
+
* Never throws and never rejects. The manifest pointer is cleared even
|
|
4215
|
+
* when the bytes could not be removed: a pointer at a half-deleted tree
|
|
4216
|
+
* would make `rollbackAddon` restore a gutted install, and the boot sweep
|
|
4217
|
+
* reclaims the residue on the next start.
|
|
4113
4218
|
*/
|
|
4114
|
-
clearBackup(packageName) {
|
|
4219
|
+
async clearBackup(packageName) {
|
|
4115
4220
|
const entry = this.manifest.get(packageName);
|
|
4116
4221
|
if (entry?.lastBackupDir == null) return;
|
|
4117
|
-
if (
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4222
|
+
if (await this.removeBackupPath(entry.lastBackupDir, "update-confirmed")) this.logger.info(`${packageName} — update confirmed, backup reclaimed`, { meta: { backupDir: entry.lastBackupDir } });
|
|
4223
|
+
try {
|
|
4224
|
+
this.manifest.upsert(packageName, {
|
|
4225
|
+
version: entry.version,
|
|
4226
|
+
source: entry.source,
|
|
4227
|
+
lastBackupDir: void 0
|
|
4228
|
+
});
|
|
4229
|
+
} catch (err) {
|
|
4230
|
+
this.logger.warn(`${packageName} — could not clear the backup pointer`, { meta: { error: require_dist.errMsg(err) } });
|
|
4231
|
+
}
|
|
4232
|
+
await this.removeEmptyBackupDirs(packageName);
|
|
4233
|
+
}
|
|
4234
|
+
/**
|
|
4235
|
+
* Boot-time retention sweep over `{addonsDir}/.backups/`.
|
|
4236
|
+
*
|
|
4237
|
+
* RULE: for each addon, the ONLY backup that survives is the one the
|
|
4238
|
+
* manifest's `lastBackupDir` points at — everything else is unreachable
|
|
4239
|
+
* by construction (no pointer ⇒ neither `rollbackAddon` nor `clearBackup`
|
|
4240
|
+
* can ever reach it). One exception: when the addon's LIVE install dir is
|
|
4241
|
+
* missing, the newest backup is the only surviving copy of that addon (a
|
|
4242
|
+
* crash between the rename-aside and the manifest write in `applyUpdate`),
|
|
4243
|
+
* so it is kept instead of reclaimed.
|
|
4244
|
+
*
|
|
4245
|
+
* Why not "delete everything once the addon is healthy": this installer
|
|
4246
|
+
* has no health signal, and the pointed-at backup is precisely the one a
|
|
4247
|
+
* failed update left for the operator's Rollback button. Bounding to one
|
|
4248
|
+
* per addon is enough — the observed 814 MiB was duplicate copies, not a
|
|
4249
|
+
* single retained rollback target.
|
|
4250
|
+
*
|
|
4251
|
+
* Never throws. Safe to run on every boot; runs before any update can be
|
|
4252
|
+
* in flight.
|
|
4253
|
+
*/
|
|
4254
|
+
async sweepBackups() {
|
|
4255
|
+
const removed = [];
|
|
4256
|
+
const kept = [];
|
|
4257
|
+
for (const packageName of this.listBackedUpPackages()) {
|
|
4258
|
+
const backups = this.listBackupsFor(packageName);
|
|
4259
|
+
if (backups.length === 0) {
|
|
4260
|
+
await this.removeEmptyBackupDirs(packageName);
|
|
4261
|
+
continue;
|
|
4262
|
+
}
|
|
4263
|
+
const keep = this.resolveBackupToKeep(packageName, backups);
|
|
4264
|
+
for (const candidate of backups) {
|
|
4265
|
+
if (keep != null && node_path.resolve(candidate) === node_path.resolve(keep)) {
|
|
4266
|
+
kept.push(candidate);
|
|
4267
|
+
continue;
|
|
4268
|
+
}
|
|
4269
|
+
if (await this.removeBackupPath(candidate, "boot-sweep")) removed.push(candidate);
|
|
4270
|
+
}
|
|
4271
|
+
await this.removeEmptyBackupDirs(packageName);
|
|
4272
|
+
}
|
|
4273
|
+
if (removed.length > 0 || kept.length > 0) this.logger.info(`Backup sweep — reclaimed ${removed.length}, retained ${kept.length} rollback target(s)`);
|
|
4274
|
+
return {
|
|
4275
|
+
removed,
|
|
4276
|
+
kept
|
|
4277
|
+
};
|
|
4278
|
+
}
|
|
4279
|
+
/**
|
|
4280
|
+
* Which backup of `packageName` survives the sweep: the manifest's
|
|
4281
|
+
* rollback target, or — when the live install is gone — the newest copy,
|
|
4282
|
+
* which is then the only one that exists. `null` means "reclaim them all".
|
|
4283
|
+
*/
|
|
4284
|
+
resolveBackupToKeep(packageName, backups) {
|
|
4285
|
+
const liveDir = node_path.join(this.addonsDir, packageName);
|
|
4286
|
+
if (!node_fs.existsSync(liveDir)) {
|
|
4287
|
+
const newest = backups[backups.length - 1] ?? null;
|
|
4288
|
+
if (newest != null) this.logger.warn(`${packageName} — live install dir missing; retaining its newest backup as the only surviving copy`, { meta: {
|
|
4289
|
+
liveDir,
|
|
4290
|
+
backupDir: newest
|
|
4291
|
+
} });
|
|
4292
|
+
return newest;
|
|
4293
|
+
}
|
|
4294
|
+
const pointer = this.manifest.get(packageName)?.lastBackupDir;
|
|
4295
|
+
if (pointer == null) return null;
|
|
4296
|
+
const resolved = node_path.resolve(pointer);
|
|
4297
|
+
return backups.some((b) => node_path.resolve(b) === resolved) ? pointer : null;
|
|
4298
|
+
}
|
|
4299
|
+
/** Package names that have a `.backups/{pkg}` directory, scoped or not. */
|
|
4300
|
+
listBackedUpPackages() {
|
|
4301
|
+
let entries;
|
|
4302
|
+
try {
|
|
4303
|
+
entries = node_fs.readdirSync(this.backupsRoot, { withFileTypes: true });
|
|
4304
|
+
} catch {
|
|
4305
|
+
return [];
|
|
4306
|
+
}
|
|
4307
|
+
const names = [];
|
|
4308
|
+
for (const entry of entries) {
|
|
4309
|
+
if (!entry.isDirectory()) continue;
|
|
4310
|
+
if (!entry.name.startsWith("@")) {
|
|
4311
|
+
names.push(entry.name);
|
|
4312
|
+
continue;
|
|
4313
|
+
}
|
|
4314
|
+
const scopeDir = node_path.join(this.backupsRoot, entry.name);
|
|
4315
|
+
for (const inner of node_fs.readdirSync(scopeDir, { withFileTypes: true })) if (inner.isDirectory()) names.push(`${entry.name}/${inner.name}`);
|
|
4316
|
+
}
|
|
4317
|
+
return names;
|
|
4126
4318
|
}
|
|
4127
4319
|
async uninstall(packageName) {
|
|
4128
4320
|
const addonDir = node_path.join(this.addonsDir, packageName);
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
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 { At as scopeKey, B as logLevelAtMost, Ct as expandCapMethods, Dt as parseJsonUnknown$1, Et as parseJsonObject, F as isVoidInput, G as objectInputDeclaresAddonId, I as kebabToCamel, J as procedureAuthKey, L as lifecycleJobSchema, M as isArrayOutputSchema, Mt as EventCategory$1, N as isCollectionArrayMethod, Ot as readinessKey, P as isObjectInput, V as looseSchema, _t as asJsonObject$1, bt as createEvent, c as RUNTIME_DEFAULTS, ct as DATAPLANE_SECRET_HEADER$1, ht as ReadinessTimeoutError, k as extractNestedAddonId, kt as resolveCapMount, lt as DEVICE_SETTINGS_CONTRIBUTION_METHODS, m as addonSettingsCapability, mt as ReadinessRegistry, ot as errMsg$1, s as METHOD_ACCESS_MAP, t as ALL_CAPABILITY_DEFINITIONS, ut as DEVICE_STATUS_METHOD, vt as asNumber, xt as emitDownForOwnedCaps, yt as asString$1 } from "./dist-BU-QPlE8.mjs";
|
|
2
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-Cp9f4dk6.mjs";
|
|
3
|
-
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-
|
|
4
|
-
import { n as getSinglePidStats, t as getPidStats } from "./resource-monitor-
|
|
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-D7-WBIg_.mjs";
|
|
5
|
+
import { n as getSinglePidStats, t as getPidStats } from "./resource-monitor-BkP504Vq.mjs";
|
|
5
6
|
import { FilesystemStorageAddon, t as FilesystemStorageProvider } from "./builtins/sqlite-storage/filesystem-storage.addon.mjs";
|
|
6
7
|
import { SqliteSettingsAddon, t as SqliteSettingsBackend } from "./builtins/sqlite-storage/sqlite-settings.addon.mjs";
|
|
7
8
|
import { ConfigStore, DeviceStore } from "./builtins/sqlite-storage/index.mjs";
|
|
@@ -17,7 +18,7 @@ import { AlertCenterAddon } from "./builtins/alerts/alerts.addon.mjs";
|
|
|
17
18
|
import "./builtins/alerts/index.mjs";
|
|
18
19
|
import { SystemConfigAddon } from "./builtins/system-config/system-config.addon.mjs";
|
|
19
20
|
import "./builtins/system-config/index.mjs";
|
|
20
|
-
import { LocalAuthAddon, a as require_ms,
|
|
21
|
+
import { LocalAuthAddon, a as require_ms, i as AuthManager, n as ApiKeyManager, o as require_safe_buffer, r as UserManager, t as ScopedTokenManager } from "./builtins/local-auth/local-auth.addon.mjs";
|
|
21
22
|
import "./builtins/local-auth/index.mjs";
|
|
22
23
|
import { DeviceManagerAddon } from "./builtins/device-manager/device-manager.addon.mjs";
|
|
23
24
|
import "./builtins/device-manager/index.mjs";
|
|
@@ -2725,21 +2726,26 @@ async function copyDirRecursive(src, dest) {
|
|
|
2725
2726
|
});
|
|
2726
2727
|
}
|
|
2727
2728
|
/**
|
|
2728
|
-
* Runtime
|
|
2729
|
-
*
|
|
2730
|
-
*
|
|
2731
|
-
*
|
|
2732
|
-
*
|
|
2729
|
+
* Runtime deps a stripped addon package.json must KEEP so the installer still
|
|
2730
|
+
* `npm install`s them into the addon dir.
|
|
2731
|
+
*
|
|
2732
|
+
* Empty by design: every native an addon can import is now HOST-EXTERNAL
|
|
2733
|
+
* (node-av, sharp, node-pty — resolved from the framework closure via the
|
|
2734
|
+
* resolver hook, see host-externals.ts), and every pure-JS lib (ssh2, werift,
|
|
2735
|
+
* @simplewebauthn, mqtt, aws-sdk, …) is inlined by the self-contained vite
|
|
2736
|
+
* bundle. So a self-contained dist requires NOTHING from a local node_modules —
|
|
2737
|
+
* `addons/` stays node_modules-free, which is what lets the backup tar the whole
|
|
2738
|
+
* folder with no exclusion list (see ADR-0027). An addon that genuinely needs an
|
|
2739
|
+
* un-bundleable runtime dep declares it explicitly in
|
|
2740
|
+
* `camstack.runtimeDependencies`; the debug escape hatch
|
|
2741
|
+
* `CAMSTACK_INSTALL_FULL_DEPS=1` restores the legacy install-everything path.
|
|
2742
|
+
*
|
|
2743
|
+
* (Historically this mirrored the build preset's COMMON_NATIVE_DEPS and
|
|
2744
|
+
* per-addon-installed node-av/sharp/onnxruntime-node/ssh2/werift/systeminformation
|
|
2745
|
+
* — 74k files / 1.34 GB of redundant node_modules on the live hub. That redundancy
|
|
2746
|
+
* is removed: the natives come from the framework, the pure-JS from the bundle.)
|
|
2733
2747
|
*/
|
|
2734
|
-
var RUNTIME_NATIVE_KEEP = [
|
|
2735
|
-
"better-sqlite3",
|
|
2736
|
-
"node-av",
|
|
2737
|
-
"onnxruntime-node",
|
|
2738
|
-
"sharp",
|
|
2739
|
-
"ssh2",
|
|
2740
|
-
"werift",
|
|
2741
|
-
"systeminformation"
|
|
2742
|
-
];
|
|
2748
|
+
var RUNTIME_NATIVE_KEEP = [];
|
|
2743
2749
|
/** Read `camstack.runtimeDependencies` (extra non-native deps the dist
|
|
2744
2750
|
* requires at runtime — versions come from the manifest's own dep maps). */
|
|
2745
2751
|
function readRuntimeDependencies(pkg) {
|
|
@@ -3926,10 +3932,14 @@ var AddonInstaller = class AddonInstaller {
|
|
|
3926
3932
|
* 1. Rename current install dir → {addonsDir}/.backups/{name}/{ts}/
|
|
3927
3933
|
* 2. Run a fresh install (npm or local depending on installSource)
|
|
3928
3934
|
* 3. Record `lastBackupDir` in the manifest entry
|
|
3935
|
+
* 4. Prune every OTHER backup of this addon — the previous one just
|
|
3936
|
+
* lost its only pointer and would otherwise be orphaned forever.
|
|
3929
3937
|
*
|
|
3930
|
-
* The post-install caller (
|
|
3931
|
-
*
|
|
3932
|
-
*
|
|
3938
|
+
* The post-install caller (restart + health-check) is responsible for
|
|
3939
|
+
* verifying the new version actually works, and for calling either
|
|
3940
|
+
* `clearBackup()` on success or `rollbackAddon()` on failure. This
|
|
3941
|
+
* method must NOT delete the backup itself: at this point the new
|
|
3942
|
+
* version has not been proven to work.
|
|
3933
3943
|
*
|
|
3934
3944
|
* Throws if there is no installed version to update — caller should
|
|
3935
3945
|
* prefer plain `install()` for first-time installs.
|
|
@@ -3982,6 +3992,7 @@ var AddonInstaller = class AddonInstaller {
|
|
|
3982
3992
|
source: this.manifest.get(packageName)?.source ?? "npm",
|
|
3983
3993
|
lastBackupDir: backupDir
|
|
3984
3994
|
});
|
|
3995
|
+
await this.pruneBackups(packageName, backupDir);
|
|
3985
3996
|
return {
|
|
3986
3997
|
name: result.name,
|
|
3987
3998
|
version: result.version,
|
|
@@ -4052,6 +4063,7 @@ var AddonInstaller = class AddonInstaller {
|
|
|
4052
4063
|
source: this.manifest.get(packageName)?.source ?? "npm",
|
|
4053
4064
|
lastBackupDir: backupDir
|
|
4054
4065
|
});
|
|
4066
|
+
await this.pruneBackups(packageName, backupDir);
|
|
4055
4067
|
return {
|
|
4056
4068
|
name: packageName,
|
|
4057
4069
|
version,
|
|
@@ -4098,23 +4110,204 @@ var AddonInstaller = class AddonInstaller {
|
|
|
4098
4110
|
this.logger.info(`${packageName} — rolled back to v${restoredVersion}`);
|
|
4099
4111
|
return restoredVersion;
|
|
4100
4112
|
}
|
|
4113
|
+
/** Root of the pre-update rollback copies: `{addonsDir}/.backups`. */
|
|
4114
|
+
get backupsRoot() {
|
|
4115
|
+
return path$39.join(this.addonsDir, ".backups");
|
|
4116
|
+
}
|
|
4117
|
+
/**
|
|
4118
|
+
* True only when `target` resolves to something strictly INSIDE
|
|
4119
|
+
* `{addonsDir}/.backups/`.
|
|
4120
|
+
*
|
|
4121
|
+
* Every deletion in this section funnels through here. `lastBackupDir`
|
|
4122
|
+
* is read back from a manifest file on disk — an operator edit, a
|
|
4123
|
+
* restored manifest from another host, or a `..` in the recorded path
|
|
4124
|
+
* must never turn "reclaim a backup" into "delete the live install".
|
|
4125
|
+
*/
|
|
4126
|
+
isInsideBackups(target) {
|
|
4127
|
+
const rel = path$39.relative(path$39.resolve(this.backupsRoot), path$39.resolve(target));
|
|
4128
|
+
return rel !== "" && !rel.startsWith("..") && !path$39.isAbsolute(rel);
|
|
4129
|
+
}
|
|
4101
4130
|
/**
|
|
4102
|
-
*
|
|
4103
|
-
*
|
|
4104
|
-
*
|
|
4131
|
+
* Best-effort removal of one path under `.backups/`.
|
|
4132
|
+
*
|
|
4133
|
+
* Never throws: an undeleted backup is a disk-space problem, whereas a
|
|
4134
|
+
* throw on the post-update success path would turn a working update into
|
|
4135
|
+
* a reported outage. Returns true only when the path is gone.
|
|
4136
|
+
*/
|
|
4137
|
+
async removeBackupPath(target, reason) {
|
|
4138
|
+
if (!this.isInsideBackups(target)) {
|
|
4139
|
+
this.logger.error("Refusing to remove a backup path outside .backups/", { meta: {
|
|
4140
|
+
target,
|
|
4141
|
+
backupsRoot: this.backupsRoot,
|
|
4142
|
+
reason
|
|
4143
|
+
} });
|
|
4144
|
+
return false;
|
|
4145
|
+
}
|
|
4146
|
+
try {
|
|
4147
|
+
await fs$17.promises.rm(target, {
|
|
4148
|
+
recursive: true,
|
|
4149
|
+
force: true
|
|
4150
|
+
});
|
|
4151
|
+
return true;
|
|
4152
|
+
} catch (err) {
|
|
4153
|
+
this.logger.warn("Backup removal failed; leaving residue for the next boot sweep", { meta: {
|
|
4154
|
+
target,
|
|
4155
|
+
reason,
|
|
4156
|
+
error: errMsg$1(err)
|
|
4157
|
+
} });
|
|
4158
|
+
return false;
|
|
4159
|
+
}
|
|
4160
|
+
}
|
|
4161
|
+
/** List the timestamped backup dirs recorded for one package. */
|
|
4162
|
+
listBackupsFor(packageName) {
|
|
4163
|
+
const dir = path$39.join(this.backupsRoot, packageName);
|
|
4164
|
+
try {
|
|
4165
|
+
return fs$17.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => path$39.join(dir, e.name)).sort();
|
|
4166
|
+
} catch {
|
|
4167
|
+
return [];
|
|
4168
|
+
}
|
|
4169
|
+
}
|
|
4170
|
+
/**
|
|
4171
|
+
* Keep at most ONE backup per addon: drop every backup dir for
|
|
4172
|
+
* `packageName` except `keepDir`.
|
|
4173
|
+
*
|
|
4174
|
+
* Called right after a new backup is recorded. Without this, a second
|
|
4175
|
+
* update overwrites `lastBackupDir` and the previous backup becomes
|
|
4176
|
+
* unreachable — no pointer, so neither `rollbackAddon` nor `clearBackup`
|
|
4177
|
+
* can ever see it again. That is how the live hub accumulated two 398 MiB
|
|
4178
|
+
* copies of one addon. Never throws.
|
|
4179
|
+
*/
|
|
4180
|
+
async pruneBackups(packageName, keepDir) {
|
|
4181
|
+
const keep = keepDir == null ? null : path$39.resolve(keepDir);
|
|
4182
|
+
const removed = [];
|
|
4183
|
+
for (const candidate of this.listBackupsFor(packageName)) {
|
|
4184
|
+
if (keep != null && path$39.resolve(candidate) === keep) continue;
|
|
4185
|
+
if (await this.removeBackupPath(candidate, "superseded-backup")) removed.push(candidate);
|
|
4186
|
+
}
|
|
4187
|
+
if (removed.length > 0) this.logger.info(`${packageName} — pruned ${removed.length} superseded backup(s)`);
|
|
4188
|
+
await this.removeEmptyBackupDirs(packageName);
|
|
4189
|
+
return removed;
|
|
4190
|
+
}
|
|
4191
|
+
/** Drop `.backups/{pkg}` (and its `@scope` parent) once they are empty. */
|
|
4192
|
+
async removeEmptyBackupDirs(packageName) {
|
|
4193
|
+
const pkgDir = path$39.join(this.backupsRoot, packageName);
|
|
4194
|
+
for (const dir of [pkgDir, path$39.dirname(pkgDir)]) {
|
|
4195
|
+
if (!this.isInsideBackups(dir)) continue;
|
|
4196
|
+
try {
|
|
4197
|
+
await fs$17.promises.rmdir(dir);
|
|
4198
|
+
} catch {
|
|
4199
|
+
return;
|
|
4200
|
+
}
|
|
4201
|
+
}
|
|
4202
|
+
}
|
|
4203
|
+
/**
|
|
4204
|
+
* Drop the recorded backup for an addon — call once the post-update
|
|
4205
|
+
* health check confirms the new version actually works.
|
|
4206
|
+
*
|
|
4207
|
+
* Never throws and never rejects. The manifest pointer is cleared even
|
|
4208
|
+
* when the bytes could not be removed: a pointer at a half-deleted tree
|
|
4209
|
+
* would make `rollbackAddon` restore a gutted install, and the boot sweep
|
|
4210
|
+
* reclaims the residue on the next start.
|
|
4105
4211
|
*/
|
|
4106
|
-
clearBackup(packageName) {
|
|
4212
|
+
async clearBackup(packageName) {
|
|
4107
4213
|
const entry = this.manifest.get(packageName);
|
|
4108
4214
|
if (entry?.lastBackupDir == null) return;
|
|
4109
|
-
if (
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4215
|
+
if (await this.removeBackupPath(entry.lastBackupDir, "update-confirmed")) this.logger.info(`${packageName} — update confirmed, backup reclaimed`, { meta: { backupDir: entry.lastBackupDir } });
|
|
4216
|
+
try {
|
|
4217
|
+
this.manifest.upsert(packageName, {
|
|
4218
|
+
version: entry.version,
|
|
4219
|
+
source: entry.source,
|
|
4220
|
+
lastBackupDir: void 0
|
|
4221
|
+
});
|
|
4222
|
+
} catch (err) {
|
|
4223
|
+
this.logger.warn(`${packageName} — could not clear the backup pointer`, { meta: { error: errMsg$1(err) } });
|
|
4224
|
+
}
|
|
4225
|
+
await this.removeEmptyBackupDirs(packageName);
|
|
4226
|
+
}
|
|
4227
|
+
/**
|
|
4228
|
+
* Boot-time retention sweep over `{addonsDir}/.backups/`.
|
|
4229
|
+
*
|
|
4230
|
+
* RULE: for each addon, the ONLY backup that survives is the one the
|
|
4231
|
+
* manifest's `lastBackupDir` points at — everything else is unreachable
|
|
4232
|
+
* by construction (no pointer ⇒ neither `rollbackAddon` nor `clearBackup`
|
|
4233
|
+
* can ever reach it). One exception: when the addon's LIVE install dir is
|
|
4234
|
+
* missing, the newest backup is the only surviving copy of that addon (a
|
|
4235
|
+
* crash between the rename-aside and the manifest write in `applyUpdate`),
|
|
4236
|
+
* so it is kept instead of reclaimed.
|
|
4237
|
+
*
|
|
4238
|
+
* Why not "delete everything once the addon is healthy": this installer
|
|
4239
|
+
* has no health signal, and the pointed-at backup is precisely the one a
|
|
4240
|
+
* failed update left for the operator's Rollback button. Bounding to one
|
|
4241
|
+
* per addon is enough — the observed 814 MiB was duplicate copies, not a
|
|
4242
|
+
* single retained rollback target.
|
|
4243
|
+
*
|
|
4244
|
+
* Never throws. Safe to run on every boot; runs before any update can be
|
|
4245
|
+
* in flight.
|
|
4246
|
+
*/
|
|
4247
|
+
async sweepBackups() {
|
|
4248
|
+
const removed = [];
|
|
4249
|
+
const kept = [];
|
|
4250
|
+
for (const packageName of this.listBackedUpPackages()) {
|
|
4251
|
+
const backups = this.listBackupsFor(packageName);
|
|
4252
|
+
if (backups.length === 0) {
|
|
4253
|
+
await this.removeEmptyBackupDirs(packageName);
|
|
4254
|
+
continue;
|
|
4255
|
+
}
|
|
4256
|
+
const keep = this.resolveBackupToKeep(packageName, backups);
|
|
4257
|
+
for (const candidate of backups) {
|
|
4258
|
+
if (keep != null && path$39.resolve(candidate) === path$39.resolve(keep)) {
|
|
4259
|
+
kept.push(candidate);
|
|
4260
|
+
continue;
|
|
4261
|
+
}
|
|
4262
|
+
if (await this.removeBackupPath(candidate, "boot-sweep")) removed.push(candidate);
|
|
4263
|
+
}
|
|
4264
|
+
await this.removeEmptyBackupDirs(packageName);
|
|
4265
|
+
}
|
|
4266
|
+
if (removed.length > 0 || kept.length > 0) this.logger.info(`Backup sweep — reclaimed ${removed.length}, retained ${kept.length} rollback target(s)`);
|
|
4267
|
+
return {
|
|
4268
|
+
removed,
|
|
4269
|
+
kept
|
|
4270
|
+
};
|
|
4271
|
+
}
|
|
4272
|
+
/**
|
|
4273
|
+
* Which backup of `packageName` survives the sweep: the manifest's
|
|
4274
|
+
* rollback target, or — when the live install is gone — the newest copy,
|
|
4275
|
+
* which is then the only one that exists. `null` means "reclaim them all".
|
|
4276
|
+
*/
|
|
4277
|
+
resolveBackupToKeep(packageName, backups) {
|
|
4278
|
+
const liveDir = path$39.join(this.addonsDir, packageName);
|
|
4279
|
+
if (!fs$17.existsSync(liveDir)) {
|
|
4280
|
+
const newest = backups[backups.length - 1] ?? null;
|
|
4281
|
+
if (newest != null) this.logger.warn(`${packageName} — live install dir missing; retaining its newest backup as the only surviving copy`, { meta: {
|
|
4282
|
+
liveDir,
|
|
4283
|
+
backupDir: newest
|
|
4284
|
+
} });
|
|
4285
|
+
return newest;
|
|
4286
|
+
}
|
|
4287
|
+
const pointer = this.manifest.get(packageName)?.lastBackupDir;
|
|
4288
|
+
if (pointer == null) return null;
|
|
4289
|
+
const resolved = path$39.resolve(pointer);
|
|
4290
|
+
return backups.some((b) => path$39.resolve(b) === resolved) ? pointer : null;
|
|
4291
|
+
}
|
|
4292
|
+
/** Package names that have a `.backups/{pkg}` directory, scoped or not. */
|
|
4293
|
+
listBackedUpPackages() {
|
|
4294
|
+
let entries;
|
|
4295
|
+
try {
|
|
4296
|
+
entries = fs$17.readdirSync(this.backupsRoot, { withFileTypes: true });
|
|
4297
|
+
} catch {
|
|
4298
|
+
return [];
|
|
4299
|
+
}
|
|
4300
|
+
const names = [];
|
|
4301
|
+
for (const entry of entries) {
|
|
4302
|
+
if (!entry.isDirectory()) continue;
|
|
4303
|
+
if (!entry.name.startsWith("@")) {
|
|
4304
|
+
names.push(entry.name);
|
|
4305
|
+
continue;
|
|
4306
|
+
}
|
|
4307
|
+
const scopeDir = path$39.join(this.backupsRoot, entry.name);
|
|
4308
|
+
for (const inner of fs$17.readdirSync(scopeDir, { withFileTypes: true })) if (inner.isDirectory()) names.push(`${entry.name}/${inner.name}`);
|
|
4309
|
+
}
|
|
4310
|
+
return names;
|
|
4118
4311
|
}
|
|
4119
4312
|
async uninstall(packageName) {
|
|
4120
4313
|
const addonDir = path$39.join(this.addonsDir, packageName);
|