@camstack/system 1.0.8 → 1.1.0

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.
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  require("./chunk-Cek0wNdY.js");
3
- const require_model_download_service = require("./model-download-service-JtVQtbb6.js");
3
+ const require_model_download_service = require("./model-download-service-1eEOkNeS.js");
4
4
  const require_custom_action_registry = require("./custom-action-registry-vLYEFTtv.js");
5
5
  exports.CustomActionRegistry = require_custom_action_registry.CustomActionRegistry;
6
6
  exports.ModelDownloadService = require_model_download_service.ModelDownloadService;
@@ -1,3 +1,3 @@
1
- import { a as ensureModel, c as isModelDownloaded, l as createFileDataPlaneHandler, n as deleteModelFromDisk, r as downloadFile, t as ModelDownloadService } from "./model-download-service-C7AjBsX9.mjs";
1
+ import { a as ensureModel, c as isModelDownloaded, l as createFileDataPlaneHandler, n as deleteModelFromDisk, r as downloadFile, t as ModelDownloadService } from "./model-download-service-RxAOiYvX.mjs";
2
2
  import { t as CustomActionRegistry } from "./custom-action-registry-BEXwC-oo.mjs";
3
3
  export { CustomActionRegistry, ModelDownloadService, createFileDataPlaneHandler, deleteModelFromDisk, downloadFile, ensureModel, isModelDownloaded };
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_model_download_service = require("./model-download-service-JtVQtbb6.js");
3
+ const require_model_download_service = require("./model-download-service-1eEOkNeS.js");
4
4
  const require_resource_monitor = require("./resource-monitor-DNNomR-i.js");
5
5
  const require_builtins_sqlite_storage_filesystem_storage_addon = require("./builtins/sqlite-storage/filesystem-storage.addon.js");
6
6
  const require_builtins_sqlite_storage_sqlite_settings_addon = require("./builtins/sqlite-storage/sqlite-settings.addon.js");
@@ -3284,6 +3284,66 @@ var AddonInstaller = class AddonInstaller {
3284
3284
  node_fs.writeFileSync(tgzPath, buf);
3285
3285
  return tgzPath;
3286
3286
  }
3287
+ /**
3288
+ * Evict an existing install dir by atomic rename-aside, then best-effort
3289
+ * delete the aside copy.
3290
+ *
3291
+ * Why not a plain `fs.promises.rm(targetDir, {recursive})`: during a
3292
+ * `camstack deploy` the addon's live runner still holds its native
3293
+ * prebuilds (`.node`/`.so`) open. On the hub's shfs/FUSE `/data/addons`,
3294
+ * a recursive rm of a held subdir fails `ENOTEMPTY` mid-walk and leaves
3295
+ * the dir gutted. A rename only touches the dirent (metadata), so it
3296
+ * succeeds with the inodes still open — the old runner keeps its fds on
3297
+ * the moved inodes until it restarts.
3298
+ *
3299
+ * The aside copy is removed best-effort: the held native libs are only
3300
+ * released once the post-install `restartAddon` recycles the runner, so a
3301
+ * failure to delete it now must NOT fail the install. Any residue is swept
3302
+ * on the next deploy.
3303
+ */
3304
+ async evictInstallDir(targetDir) {
3305
+ if (!node_fs.existsSync(targetDir)) return;
3306
+ const graveyard = node_path.join(this.addonsDir, ".evicting");
3307
+ ensureDir(graveyard);
3308
+ await this.sweepGraveyard(graveyard);
3309
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3310
+ const flatName = node_path.relative(this.addonsDir, targetDir).replace(/[/\\]/g, "-");
3311
+ const asideDir = node_path.join(graveyard, `${flatName}-${ts}`);
3312
+ try {
3313
+ node_fs.renameSync(targetDir, asideDir);
3314
+ } catch (renameErr) {
3315
+ if (renameErr.code === "EXDEV") {
3316
+ await node_fs.promises.rm(targetDir, {
3317
+ recursive: true,
3318
+ force: true
3319
+ });
3320
+ return;
3321
+ }
3322
+ throw renameErr;
3323
+ }
3324
+ await node_fs.promises.rm(asideDir, {
3325
+ recursive: true,
3326
+ force: true
3327
+ }).catch((cleanupErr) => {
3328
+ this.logger.debug("evictInstallDir: aside cleanup deferred (likely held open)", { meta: {
3329
+ asideDir,
3330
+ error: (0, _camstack_types.errMsg)(cleanupErr)
3331
+ } });
3332
+ });
3333
+ }
3334
+ /** Best-effort removal of leftover `.evicting/*` dirs from earlier deploys. */
3335
+ async sweepGraveyard(graveyard) {
3336
+ let entries;
3337
+ try {
3338
+ entries = node_fs.readdirSync(graveyard);
3339
+ } catch {
3340
+ return;
3341
+ }
3342
+ await Promise.all(entries.map((name) => node_fs.promises.rm(node_path.join(graveyard, name), {
3343
+ recursive: true,
3344
+ force: true
3345
+ }).catch(() => void 0)));
3346
+ }
3287
3347
  /** Install addon from a tgz file (uploaded or downloaded) */
3288
3348
  async installFromTgz(tgzPath) {
3289
3349
  const tmpDir = node_fs.mkdtempSync(node_path.join(node_os.tmpdir(), "camstack-addon-install-"));
@@ -3301,10 +3361,7 @@ var AddonInstaller = class AddonInstaller {
3301
3361
  if (!pkgView) throw new Error(`Invalid package.json at ${pkgJsonPath}`);
3302
3362
  if (!pkgView.camstackAddons) throw new Error(`Package ${pkgView.name} has no camstack.addons manifest`);
3303
3363
  const targetDir = node_path.join(this.addonsDir, pkgView.name);
3304
- await node_fs.promises.rm(targetDir, {
3305
- recursive: true,
3306
- force: true
3307
- });
3364
+ await this.evictInstallDir(targetDir);
3308
3365
  ensureDir(targetDir);
3309
3366
  const sourceDir = node_path.dirname(pkgJsonPath);
3310
3367
  const strippedManifest = stripCamstackDeps(pkgView.raw);
@@ -89904,6 +89961,15 @@ function normalizeHubUrl(raw, defaultPort) {
89904
89961
  return `${/:\d+$/.test(hostPort) ? hostPort : `${hostPort}:${defaultPort}`}/${nodeId}`;
89905
89962
  }
89906
89963
  //#endregion
89964
+ //#region src/kernel/moleculer/addon-deploy-source.ts
89965
+ function isAddonDeploySource(v) {
89966
+ if (!v || typeof v !== "object") return false;
89967
+ const o = v;
89968
+ if (o["kind"] === "npm") return typeof o["version"] === "string";
89969
+ if (o["kind"] === "hub-http") return typeof o["url"] === "string" && typeof o["token"] === "string" && typeof o["sha256"] === "string" && typeof o["bytes"] === "number";
89970
+ return false;
89971
+ }
89972
+ //#endregion
89907
89973
  //#region src/kernel/moleculer/core-cap-service.ts
89908
89974
  /**
89909
89975
  * Default Moleculer service name for the hub core-capability bridge.
@@ -91755,6 +91821,7 @@ Object.defineProperty(exports, "installPythonRequirements", {
91755
91821
  });
91756
91822
  exports.ipcChildLink = require_manifest_python_deps.ipcChildLink;
91757
91823
  exports.ipcParentLink = require_manifest_python_deps.ipcParentLink;
91824
+ exports.isAddonDeploySource = isAddonDeploySource;
91758
91825
  exports.isClusterSecretMismatchError = isClusterSecretMismatchError;
91759
91826
  exports.isInfraCapability = isInfraCapability;
91760
91827
  exports.isModelDownloaded = require_model_download_service.isModelDownloaded;
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 { a as ensureModel, c as isModelDownloaded, d as createAuthenticatedFileServer, f as parseRangeHeader, i as downloadModel, l as createFileDataPlaneHandler, m as resolveFilePath, n as deleteModelFromDisk, o as fetchJson, p as parseTokenizedUrl, r as downloadFile, s as getModelFilePath, t as ModelDownloadService, u as contentTypeFor } from "./model-download-service-C7AjBsX9.mjs";
2
+ import { a as ensureModel, c as isModelDownloaded, d as createAuthenticatedFileServer, f as parseRangeHeader, i as downloadModel, l as createFileDataPlaneHandler, m as resolveFilePath, n as deleteModelFromDisk, o as fetchJson, p as parseTokenizedUrl, r as downloadFile, s as getModelFilePath, t as ModelDownloadService, u as contentTypeFor } from "./model-download-service-RxAOiYvX.mjs";
3
3
  import { n as getSinglePidStats, t as getPidStats } from "./resource-monitor-BkP504Vq.mjs";
4
4
  import { FilesystemStorageAddon, t as FilesystemStorageProvider } from "./builtins/sqlite-storage/filesystem-storage.addon.mjs";
5
5
  import { SqliteSettingsAddon, t as SqliteSettingsBackend } from "./builtins/sqlite-storage/sqlite-settings.addon.mjs";
@@ -3278,6 +3278,66 @@ var AddonInstaller = class AddonInstaller {
3278
3278
  fs$17.writeFileSync(tgzPath, buf);
3279
3279
  return tgzPath;
3280
3280
  }
3281
+ /**
3282
+ * Evict an existing install dir by atomic rename-aside, then best-effort
3283
+ * delete the aside copy.
3284
+ *
3285
+ * Why not a plain `fs.promises.rm(targetDir, {recursive})`: during a
3286
+ * `camstack deploy` the addon's live runner still holds its native
3287
+ * prebuilds (`.node`/`.so`) open. On the hub's shfs/FUSE `/data/addons`,
3288
+ * a recursive rm of a held subdir fails `ENOTEMPTY` mid-walk and leaves
3289
+ * the dir gutted. A rename only touches the dirent (metadata), so it
3290
+ * succeeds with the inodes still open — the old runner keeps its fds on
3291
+ * the moved inodes until it restarts.
3292
+ *
3293
+ * The aside copy is removed best-effort: the held native libs are only
3294
+ * released once the post-install `restartAddon` recycles the runner, so a
3295
+ * failure to delete it now must NOT fail the install. Any residue is swept
3296
+ * on the next deploy.
3297
+ */
3298
+ async evictInstallDir(targetDir) {
3299
+ if (!fs$17.existsSync(targetDir)) return;
3300
+ const graveyard = path$40.join(this.addonsDir, ".evicting");
3301
+ ensureDir(graveyard);
3302
+ await this.sweepGraveyard(graveyard);
3303
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3304
+ const flatName = path$40.relative(this.addonsDir, targetDir).replace(/[/\\]/g, "-");
3305
+ const asideDir = path$40.join(graveyard, `${flatName}-${ts}`);
3306
+ try {
3307
+ fs$17.renameSync(targetDir, asideDir);
3308
+ } catch (renameErr) {
3309
+ if (renameErr.code === "EXDEV") {
3310
+ await fs$17.promises.rm(targetDir, {
3311
+ recursive: true,
3312
+ force: true
3313
+ });
3314
+ return;
3315
+ }
3316
+ throw renameErr;
3317
+ }
3318
+ await fs$17.promises.rm(asideDir, {
3319
+ recursive: true,
3320
+ force: true
3321
+ }).catch((cleanupErr) => {
3322
+ this.logger.debug("evictInstallDir: aside cleanup deferred (likely held open)", { meta: {
3323
+ asideDir,
3324
+ error: errMsg(cleanupErr)
3325
+ } });
3326
+ });
3327
+ }
3328
+ /** Best-effort removal of leftover `.evicting/*` dirs from earlier deploys. */
3329
+ async sweepGraveyard(graveyard) {
3330
+ let entries;
3331
+ try {
3332
+ entries = fs$17.readdirSync(graveyard);
3333
+ } catch {
3334
+ return;
3335
+ }
3336
+ await Promise.all(entries.map((name) => fs$17.promises.rm(path$40.join(graveyard, name), {
3337
+ recursive: true,
3338
+ force: true
3339
+ }).catch(() => void 0)));
3340
+ }
3281
3341
  /** Install addon from a tgz file (uploaded or downloaded) */
3282
3342
  async installFromTgz(tgzPath) {
3283
3343
  const tmpDir = fs$17.mkdtempSync(path$40.join(os$17.tmpdir(), "camstack-addon-install-"));
@@ -3295,10 +3355,7 @@ var AddonInstaller = class AddonInstaller {
3295
3355
  if (!pkgView) throw new Error(`Invalid package.json at ${pkgJsonPath}`);
3296
3356
  if (!pkgView.camstackAddons) throw new Error(`Package ${pkgView.name} has no camstack.addons manifest`);
3297
3357
  const targetDir = path$40.join(this.addonsDir, pkgView.name);
3298
- await fs$17.promises.rm(targetDir, {
3299
- recursive: true,
3300
- force: true
3301
- });
3358
+ await this.evictInstallDir(targetDir);
3302
3359
  ensureDir(targetDir);
3303
3360
  const sourceDir = path$40.dirname(pkgJsonPath);
3304
3361
  const strippedManifest = stripCamstackDeps(pkgView.raw);
@@ -89898,6 +89955,15 @@ function normalizeHubUrl(raw, defaultPort) {
89898
89955
  return `${/:\d+$/.test(hostPort) ? hostPort : `${hostPort}:${defaultPort}`}/${nodeId}`;
89899
89956
  }
89900
89957
  //#endregion
89958
+ //#region src/kernel/moleculer/addon-deploy-source.ts
89959
+ function isAddonDeploySource(v) {
89960
+ if (!v || typeof v !== "object") return false;
89961
+ const o = v;
89962
+ if (o["kind"] === "npm") return typeof o["version"] === "string";
89963
+ if (o["kind"] === "hub-http") return typeof o["url"] === "string" && typeof o["token"] === "string" && typeof o["sha256"] === "string" && typeof o["bytes"] === "number";
89964
+ return false;
89965
+ }
89966
+ //#endregion
89901
89967
  //#region src/kernel/moleculer/core-cap-service.ts
89902
89968
  /**
89903
89969
  * Default Moleculer service name for the hub core-capability bridge.
@@ -91492,4 +91558,4 @@ async function stageFrameworkLockstep(input) {
91492
91558
  return results;
91493
91559
  }
91494
91560
  //#endregion
91495
- export { AGENT_CAP_FWD_ACTION, AGENT_CAP_FWD_SERVICE, AddonApiFactory, AddonDepsManager, AddonEngineManager, AddonHealthMonitor, AddonInstaller, AddonLoader, AddonManifest, AddonRouteRegistry, AlertCenterAddon, ApiKeyManager, AuthManager, CLUSTER_SECRET_MISMATCH_TYPE, CLUSTER_SECRET_REJECTED_EXIT_CODE, CORE_CAP_SERVICE_NAME, CapRouteError, CapRouteResolver, CapUsageRegistry, CapabilityHandle, CapabilityRegistry, CapabilityUnavailableError, ConfigManager, ConfigStore, ConsoleDestination, ConsoleLoggingAddon, CustomActionRegistry, DEFAULT_DATA_PATH, DataPlaneRegistry, DeviceManagerAddon, DeviceRegistry, DeviceStore, EVENT_TOPIC_PREFIX, EngineManagerResolver, EventBus, FRAMEWORK_LOCKSTEP, FeatureManager, FilesystemStorageAddon, FilesystemStorageProvider, FrameDecoder, FsStorageBackend, HEALTH_MONITOR_GRACE_PERIOD_MS, HEALTH_MONITOR_RETRY_INTERVALS_MS, HEALTH_MONITOR_TICK_MS, HubForwarderAddon, HubForwarderDestination, HubLogForwarder, HubNodeRegistry, INFRA_CAPABILITIES, IntegrationRegistry, JobJournal, LifecycleJobEngine, LifecycleStateMachine, LocalAuthAddon, LocalChildClient, LocalChildRegistry, LogManager, LogRingBuffer, ModelDownloadService, NATIVE_PROVIDER_SERVICE_INFIX, NativeMetricsAddon, NativeMetricsProvider, NetworkQualityTracker, NotificationService, PYTHON_VERSION, PipelineRunner, PipelineValidator, PythonEnvManager, RESTART_MARKER_FILE, RUNTIME_DEFAULTS, ReadinessRegistry, ReadinessTimeoutError, ReplEngine, RingBuffer, ScopedLogger, ScopedTokenManager, SocketChannel, SqliteSettingsAddon, SqliteSettingsBackend, StagingArea, StorageLocationManager, StorageManager, StorageOrchestratorAddon, StorageOrchestratorService, SystemConfigAddon, SystemEventBus, ToastService, UDS_NO_ROUTE_PREFIX, UdsLocalTransportClient, UdsLocalTransportServer, UserManager, WinstonDestination, WinstonLoggingAddon, __resetCapUsageRegistryForTests, adaptBrokerToCluster, bootstrapSchema, brokerCallForCap, brokerTransportLink, buildBinaryPath, buildLinkChain, buildNativeCapProxy, buildNodeManifest, buildStorageLocationRegistry, buildUdsNativeCapProxy, callRegisterNodeWithRetry, callWithServiceDiscovery, capActionName, capActionSuffix, capBareAction, capServiceName, classifyCapRoute, clearPendingRestart, clusterSecretMatches, contentTypeFor, copyDirRecursive, copyExtraFileDirs, createAddonContext, createAddonService, createAuthenticatedFileServer, createBroker, createBrokerDeviceManagerApi, createCoreCapService, createFileDataPlaneHandler, createHubService, createHwAccelService, createKernelHwAccel, createLocalTransport, createParentUnownedCallHandler, createProcessService, createReadinessService, createReadinessServiceForRegistry, createScopedProcessManager, createStreamProbeBrokerService, createUdsAddonContext, createUdsEventBridge, createUdsEventBus, createUdsLogger, createUdsLoggerWithControl, deleteModelFromDisk, deriveAgentListenPort, describeProviderKindDrift, detectWorkspacePackagesDir, downloadBinary, downloadFile, downloadModel, emitDownForOwnedCaps, encodeFrame, ensureBinary, ensureDir, ensureFfmpeg, ensureLibraryBuilt, ensureModel, ensurePython, ensureTlsCert, fetchJson, findInPath, formatLogLine, getBrokerEventBus, getCapUsageRegistry, getFfmpegDownloadUrl, getModelFilePath, getOrInitReadinessRegistry, getOrInitReadinessRegistryForClient, getPidStats, getPlatformInfo, getPythonDownloadUrl, getRestartMarkerPath, getSinglePidStats, getWorkerDeviceRegistry, hashClusterSecret, installManifestNativeDeps, installManifestPythonDeps, installPackageFromNpm, installPythonPackages, installPythonRequirements, ipcChildLink, ipcParentLink, isClusterSecretMismatchError, isInfraCapability, isModelDownloaded, isSourceNewer, loadTlsCert, localEndpointPath, localProviderLink, mountNativeCapService, parseCapAction, parseRangeHeader, parseTokenizedUrl, proxyToUpstream, readPendingRestart, readinessKey, registerEventBusService, resolveFilePath, resolveHwAccel, scheduleSelfRestart, scopeKey, scopesAllowDeviceCap, serializeTypedArrays, setHubConnected, stageFrameworkLockstep, stripCamstackDeps, udsChildLogToWorkerEntry, validateProviderRegistrations, writePendingRestart };
91561
+ export { AGENT_CAP_FWD_ACTION, AGENT_CAP_FWD_SERVICE, AddonApiFactory, AddonDepsManager, AddonEngineManager, AddonHealthMonitor, AddonInstaller, AddonLoader, AddonManifest, AddonRouteRegistry, AlertCenterAddon, ApiKeyManager, AuthManager, CLUSTER_SECRET_MISMATCH_TYPE, CLUSTER_SECRET_REJECTED_EXIT_CODE, CORE_CAP_SERVICE_NAME, CapRouteError, CapRouteResolver, CapUsageRegistry, CapabilityHandle, CapabilityRegistry, CapabilityUnavailableError, ConfigManager, ConfigStore, ConsoleDestination, ConsoleLoggingAddon, CustomActionRegistry, DEFAULT_DATA_PATH, DataPlaneRegistry, DeviceManagerAddon, DeviceRegistry, DeviceStore, EVENT_TOPIC_PREFIX, EngineManagerResolver, EventBus, FRAMEWORK_LOCKSTEP, FeatureManager, FilesystemStorageAddon, FilesystemStorageProvider, FrameDecoder, FsStorageBackend, HEALTH_MONITOR_GRACE_PERIOD_MS, HEALTH_MONITOR_RETRY_INTERVALS_MS, HEALTH_MONITOR_TICK_MS, HubForwarderAddon, HubForwarderDestination, HubLogForwarder, HubNodeRegistry, INFRA_CAPABILITIES, IntegrationRegistry, JobJournal, LifecycleJobEngine, LifecycleStateMachine, LocalAuthAddon, LocalChildClient, LocalChildRegistry, LogManager, LogRingBuffer, ModelDownloadService, NATIVE_PROVIDER_SERVICE_INFIX, NativeMetricsAddon, NativeMetricsProvider, NetworkQualityTracker, NotificationService, PYTHON_VERSION, PipelineRunner, PipelineValidator, PythonEnvManager, RESTART_MARKER_FILE, RUNTIME_DEFAULTS, ReadinessRegistry, ReadinessTimeoutError, ReplEngine, RingBuffer, ScopedLogger, ScopedTokenManager, SocketChannel, SqliteSettingsAddon, SqliteSettingsBackend, StagingArea, StorageLocationManager, StorageManager, StorageOrchestratorAddon, StorageOrchestratorService, SystemConfigAddon, SystemEventBus, ToastService, UDS_NO_ROUTE_PREFIX, UdsLocalTransportClient, UdsLocalTransportServer, UserManager, WinstonDestination, WinstonLoggingAddon, __resetCapUsageRegistryForTests, adaptBrokerToCluster, bootstrapSchema, brokerCallForCap, brokerTransportLink, buildBinaryPath, buildLinkChain, buildNativeCapProxy, buildNodeManifest, buildStorageLocationRegistry, buildUdsNativeCapProxy, callRegisterNodeWithRetry, callWithServiceDiscovery, capActionName, capActionSuffix, capBareAction, capServiceName, classifyCapRoute, clearPendingRestart, clusterSecretMatches, contentTypeFor, copyDirRecursive, copyExtraFileDirs, createAddonContext, createAddonService, createAuthenticatedFileServer, createBroker, createBrokerDeviceManagerApi, createCoreCapService, createFileDataPlaneHandler, createHubService, createHwAccelService, createKernelHwAccel, createLocalTransport, createParentUnownedCallHandler, createProcessService, createReadinessService, createReadinessServiceForRegistry, createScopedProcessManager, createStreamProbeBrokerService, createUdsAddonContext, createUdsEventBridge, createUdsEventBus, createUdsLogger, createUdsLoggerWithControl, deleteModelFromDisk, deriveAgentListenPort, describeProviderKindDrift, detectWorkspacePackagesDir, downloadBinary, downloadFile, downloadModel, emitDownForOwnedCaps, encodeFrame, ensureBinary, ensureDir, ensureFfmpeg, ensureLibraryBuilt, ensureModel, ensurePython, ensureTlsCert, fetchJson, findInPath, formatLogLine, getBrokerEventBus, getCapUsageRegistry, getFfmpegDownloadUrl, getModelFilePath, getOrInitReadinessRegistry, getOrInitReadinessRegistryForClient, getPidStats, getPlatformInfo, getPythonDownloadUrl, getRestartMarkerPath, getSinglePidStats, getWorkerDeviceRegistry, hashClusterSecret, installManifestNativeDeps, installManifestPythonDeps, installPackageFromNpm, installPythonPackages, installPythonRequirements, ipcChildLink, ipcParentLink, isAddonDeploySource, isClusterSecretMismatchError, isInfraCapability, isModelDownloaded, isSourceNewer, loadTlsCert, localEndpointPath, localProviderLink, mountNativeCapService, parseCapAction, parseRangeHeader, parseTokenizedUrl, proxyToUpstream, readPendingRestart, readinessKey, registerEventBusService, resolveFilePath, resolveHwAccel, scheduleSelfRestart, scopeKey, scopesAllowDeviceCap, serializeTypedArrays, setHubConnected, stageFrameworkLockstep, stripCamstackDeps, udsChildLogToWorkerEntry, validateProviderRegistrations, writePendingRestart };
@@ -167,6 +167,26 @@ export declare class AddonInstaller {
167
167
  * the legacy fallback path remains the right answer for those.
168
168
  */
169
169
  private downloadNpmTarball;
170
+ /**
171
+ * Evict an existing install dir by atomic rename-aside, then best-effort
172
+ * delete the aside copy.
173
+ *
174
+ * Why not a plain `fs.promises.rm(targetDir, {recursive})`: during a
175
+ * `camstack deploy` the addon's live runner still holds its native
176
+ * prebuilds (`.node`/`.so`) open. On the hub's shfs/FUSE `/data/addons`,
177
+ * a recursive rm of a held subdir fails `ENOTEMPTY` mid-walk and leaves
178
+ * the dir gutted. A rename only touches the dirent (metadata), so it
179
+ * succeeds with the inodes still open — the old runner keeps its fds on
180
+ * the moved inodes until it restarts.
181
+ *
182
+ * The aside copy is removed best-effort: the held native libs are only
183
+ * released once the post-install `restartAddon` recycles the runner, so a
184
+ * failure to delete it now must NOT fail the install. Any residue is swept
185
+ * on the next deploy.
186
+ */
187
+ private evictInstallDir;
188
+ /** Best-effort removal of leftover `.evicting/*` dirs from earlier deploys. */
189
+ private sweepGraveyard;
170
190
  /** Install addon from a tgz file (uploaded or downloaded) */
171
191
  installFromTgz(tgzPath: string): Promise<{
172
192
  name: string;
@@ -27,6 +27,7 @@ export { buildStorageLocationRegistry } from './storage-location-registry.js';
27
27
  export type { StorageLocationRegistry } from './storage-location-registry.js';
28
28
  export { createBroker, deriveAgentListenPort } from './moleculer/broker-factory.js';
29
29
  export type { BrokerConfig } from './moleculer/broker-factory.js';
30
+ export { type AddonDeploySource, isAddonDeploySource } from './moleculer/addon-deploy-source.js';
30
31
  export { createAddonService, validateProviderRegistrations, } from './moleculer/addon-service-factory.js';
31
32
  export { createCoreCapService, CORE_CAP_SERVICE_NAME } from './moleculer/core-cap-service.js';
32
33
  export type { CoreCapAction, CoreCapServiceOptions } from './moleculer/core-cap-service.js';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Where an agent should fetch an addon's bytes from for a `$agent.deploy`.
3
+ * `npm`: the agent runs its own AddonInstaller pull. `hub-http`: the agent
4
+ * streams the staged tgz from the hub's one-time-token bundle route.
5
+ */
6
+ export type AddonDeploySource = {
7
+ readonly kind: 'npm';
8
+ readonly version: string;
9
+ } | {
10
+ readonly kind: 'hub-http';
11
+ readonly url: string;
12
+ readonly token: string;
13
+ readonly sha256: string;
14
+ readonly bytes: number;
15
+ };
16
+ export declare function isAddonDeploySource(v: unknown): v is AddonDeploySource;
@@ -301,6 +301,24 @@ function createFileDataPlaneHandler(opts) {
301
301
  }
302
302
  //#endregion
303
303
  //#region src/download/model-downloader.ts
304
+ function isNonEmptyFile(filePath) {
305
+ return node_fs.existsSync(filePath) && node_fs.statSync(filePath).size > 0;
306
+ }
307
+ /**
308
+ * Sibling files of a single-file (non-directory) format — extra files the
309
+ * format needs, fetched from the same remote directory as `url` and stored
310
+ * flat alongside the main file in `modelsDir`. Catalog-declared via
311
+ * `formatEntry.files`, e.g. OpenVINO IR lists its `.bin` weights next to the
312
+ * `.xml`. The downloader stays format-agnostic; the convention lives in the
313
+ * catalog data (like `MLPACKAGE_FILES` for the directory case).
314
+ */
315
+ function siblingFilesFor(formatEntry) {
316
+ return formatEntry.isDirectory ? [] : formatEntry.files ?? [];
317
+ }
318
+ /** Resolve a sibling's remote URL relative to the main file's directory. */
319
+ function siblingUrl(mainUrl, sibling) {
320
+ return mainUrl.replace(/[^/]+$/, sibling);
321
+ }
304
322
  /** Build fetch headers, including HF auth token for huggingface.co URLs */
305
323
  function buildHeaders(url) {
306
324
  const headers = { "User-Agent": "CamStack/1.0" };
@@ -466,14 +484,18 @@ async function ensureModel(modelsDir, entry, format, onProgress) {
466
484
  if (entry.extraFiles) for (const extra of entry.extraFiles) await downloadFile(extra.url, node_path.join(modelsDir, extra.filename));
467
485
  const filename = formatEntry.url.split("/").pop() ?? `${entry.id}.${format}`;
468
486
  const modelPath = node_path.join(modelsDir, filename);
487
+ const siblings = siblingFilesFor(formatEntry);
469
488
  if (node_fs.existsSync(modelPath)) if (formatEntry.isDirectory && !node_fs.existsSync(node_path.join(modelPath, "Manifest.json"))) node_fs.rmSync(modelPath, {
470
489
  recursive: true,
471
490
  force: true
472
491
  });
473
- else return modelPath;
492
+ else if (siblings.some((f) => !isNonEmptyFile(node_path.join(modelsDir, f)))) {} else return modelPath;
474
493
  node_fs.mkdirSync(modelsDir, { recursive: true });
475
494
  if (formatEntry.isDirectory) await downloadDirectory(formatEntry.url, modelPath, formatEntry.files, onProgress);
476
- else await downloadFile(formatEntry.url, modelPath, (downloaded, total) => onProgress?.(downloaded, total === 0 ? void 0 : total));
495
+ else {
496
+ await downloadFile(formatEntry.url, modelPath, (downloaded, total) => onProgress?.(downloaded, total === 0 ? void 0 : total));
497
+ for (const sibling of siblings) await downloadFile(siblingUrl(formatEntry.url, sibling), node_path.join(modelsDir, sibling));
498
+ }
477
499
  return modelPath;
478
500
  }
479
501
  /** Compute the on-disk path for a given model + format, even when not yet downloaded. */
@@ -490,17 +512,25 @@ function isModelDownloaded(modelsDir, entry, format) {
490
512
  const modelPath = getModelFilePath(modelsDir, entry, format);
491
513
  if (!modelPath || !node_fs.existsSync(modelPath)) return false;
492
514
  if (formatEntry.isDirectory) return node_fs.existsSync(node_path.join(modelPath, "Manifest.json"));
493
- return node_fs.statSync(modelPath).size > 0;
515
+ if (node_fs.statSync(modelPath).size <= 0) return false;
516
+ return siblingFilesFor(formatEntry).every((f) => isNonEmptyFile(node_path.join(modelsDir, f)));
494
517
  }
495
518
  /** Remove the on-disk model file/directory. Returns true if something was deleted. */
496
519
  function deleteModelFromDisk(modelsDir, entry, format) {
497
520
  const modelPath = getModelFilePath(modelsDir, entry, format);
498
521
  if (!modelPath || !node_fs.existsSync(modelPath)) return false;
499
- if (entry.formats[format]?.isDirectory) node_fs.rmSync(modelPath, {
522
+ const formatEntry = entry.formats[format];
523
+ if (formatEntry?.isDirectory) node_fs.rmSync(modelPath, {
500
524
  recursive: true,
501
525
  force: true
502
526
  });
503
- else node_fs.unlinkSync(modelPath);
527
+ else {
528
+ node_fs.unlinkSync(modelPath);
529
+ if (formatEntry) for (const sibling of siblingFilesFor(formatEntry)) {
530
+ const sibPath = node_path.join(modelsDir, sibling);
531
+ if (node_fs.existsSync(sibPath)) node_fs.unlinkSync(sibPath);
532
+ }
533
+ }
504
534
  return true;
505
535
  }
506
536
  //#endregion
@@ -300,6 +300,24 @@ function createFileDataPlaneHandler(opts) {
300
300
  }
301
301
  //#endregion
302
302
  //#region src/download/model-downloader.ts
303
+ function isNonEmptyFile(filePath) {
304
+ return fs.existsSync(filePath) && fs.statSync(filePath).size > 0;
305
+ }
306
+ /**
307
+ * Sibling files of a single-file (non-directory) format — extra files the
308
+ * format needs, fetched from the same remote directory as `url` and stored
309
+ * flat alongside the main file in `modelsDir`. Catalog-declared via
310
+ * `formatEntry.files`, e.g. OpenVINO IR lists its `.bin` weights next to the
311
+ * `.xml`. The downloader stays format-agnostic; the convention lives in the
312
+ * catalog data (like `MLPACKAGE_FILES` for the directory case).
313
+ */
314
+ function siblingFilesFor(formatEntry) {
315
+ return formatEntry.isDirectory ? [] : formatEntry.files ?? [];
316
+ }
317
+ /** Resolve a sibling's remote URL relative to the main file's directory. */
318
+ function siblingUrl(mainUrl, sibling) {
319
+ return mainUrl.replace(/[^/]+$/, sibling);
320
+ }
303
321
  /** Build fetch headers, including HF auth token for huggingface.co URLs */
304
322
  function buildHeaders(url) {
305
323
  const headers = { "User-Agent": "CamStack/1.0" };
@@ -465,14 +483,18 @@ async function ensureModel(modelsDir, entry, format, onProgress) {
465
483
  if (entry.extraFiles) for (const extra of entry.extraFiles) await downloadFile(extra.url, path$1.join(modelsDir, extra.filename));
466
484
  const filename = formatEntry.url.split("/").pop() ?? `${entry.id}.${format}`;
467
485
  const modelPath = path$1.join(modelsDir, filename);
486
+ const siblings = siblingFilesFor(formatEntry);
468
487
  if (fs.existsSync(modelPath)) if (formatEntry.isDirectory && !fs.existsSync(path$1.join(modelPath, "Manifest.json"))) fs.rmSync(modelPath, {
469
488
  recursive: true,
470
489
  force: true
471
490
  });
472
- else return modelPath;
491
+ else if (siblings.some((f) => !isNonEmptyFile(path$1.join(modelsDir, f)))) {} else return modelPath;
473
492
  fs.mkdirSync(modelsDir, { recursive: true });
474
493
  if (formatEntry.isDirectory) await downloadDirectory(formatEntry.url, modelPath, formatEntry.files, onProgress);
475
- else await downloadFile(formatEntry.url, modelPath, (downloaded, total) => onProgress?.(downloaded, total === 0 ? void 0 : total));
494
+ else {
495
+ await downloadFile(formatEntry.url, modelPath, (downloaded, total) => onProgress?.(downloaded, total === 0 ? void 0 : total));
496
+ for (const sibling of siblings) await downloadFile(siblingUrl(formatEntry.url, sibling), path$1.join(modelsDir, sibling));
497
+ }
476
498
  return modelPath;
477
499
  }
478
500
  /** Compute the on-disk path for a given model + format, even when not yet downloaded. */
@@ -489,17 +511,25 @@ function isModelDownloaded(modelsDir, entry, format) {
489
511
  const modelPath = getModelFilePath(modelsDir, entry, format);
490
512
  if (!modelPath || !fs.existsSync(modelPath)) return false;
491
513
  if (formatEntry.isDirectory) return fs.existsSync(path$1.join(modelPath, "Manifest.json"));
492
- return fs.statSync(modelPath).size > 0;
514
+ if (fs.statSync(modelPath).size <= 0) return false;
515
+ return siblingFilesFor(formatEntry).every((f) => isNonEmptyFile(path$1.join(modelsDir, f)));
493
516
  }
494
517
  /** Remove the on-disk model file/directory. Returns true if something was deleted. */
495
518
  function deleteModelFromDisk(modelsDir, entry, format) {
496
519
  const modelPath = getModelFilePath(modelsDir, entry, format);
497
520
  if (!modelPath || !fs.existsSync(modelPath)) return false;
498
- if (entry.formats[format]?.isDirectory) fs.rmSync(modelPath, {
521
+ const formatEntry = entry.formats[format];
522
+ if (formatEntry?.isDirectory) fs.rmSync(modelPath, {
499
523
  recursive: true,
500
524
  force: true
501
525
  });
502
- else fs.unlinkSync(modelPath);
526
+ else {
527
+ fs.unlinkSync(modelPath);
528
+ if (formatEntry) for (const sibling of siblingFilesFor(formatEntry)) {
529
+ const sibPath = path$1.join(modelsDir, sibling);
530
+ if (fs.existsSync(sibPath)) fs.unlinkSync(sibPath);
531
+ }
532
+ }
503
533
  return true;
504
534
  }
505
535
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/system",
3
- "version": "1.0.8",
3
+ "version": "1.1.0",
4
4
  "description": "Core addon for CamStack — builtins, pipeline, process management, auth, logging, events",
5
5
  "keywords": [
6
6
  "camstack",