@camstack/system 1.0.5 → 1.0.7

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/index.mjs CHANGED
@@ -1,5 +1,6 @@
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 { n as getSinglePidStats, t as getPidStats } from "./resource-monitor-ClDGFyf6.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";
3
+ import { n as getSinglePidStats, t as getPidStats } from "./resource-monitor-BkP504Vq.mjs";
3
4
  import { FilesystemStorageAddon, t as FilesystemStorageProvider } from "./builtins/sqlite-storage/filesystem-storage.addon.mjs";
4
5
  import { SqliteSettingsAddon, t as SqliteSettingsBackend } from "./builtins/sqlite-storage/sqlite-settings.addon.mjs";
5
6
  import { ConfigStore, DeviceStore } from "./builtins/sqlite-storage/index.mjs";
@@ -18,14 +19,15 @@ import { LocalAuthAddon, a as require_safe_buffer, i as AuthManager, n as ApiKey
18
19
  import "./builtins/local-auth/index.mjs";
19
20
  import { DeviceManagerAddon } from "./builtins/device-manager/device-manager.addon.mjs";
20
21
  import "./builtins/device-manager/index.mjs";
21
- import { A as createUdsLoggerWithControl, B as createLocalTransport, C as ipcParentLink, D as createUdsEventBus, E as createUdsEventBridge, F as CapRouteError, G as FrameDecoder, H as UdsLocalTransportServer, I as classifyCapRoute, J as buildUdsNativeCapProxy, K as encodeFrame, L as LocalChildClient, M as AGENT_CAP_FWD_SERVICE, N as CapRouteResolver, O as udsChildLogToWorkerEntry, P as callWithServiceDiscovery, Q as mountNativeCapService, R as LocalChildRegistry, S as ipcChildLink, T as createParentUnownedCallHandler, U as SocketChannel, V as UdsLocalTransportClient, W as localEndpointPath, Y as createBrokerDeviceManagerApi, _ as __resetCapUsageRegistryForTests, a as getWorkerDeviceRegistry, at as capBareAction, b as brokerTransportLink, c as setHubConnected, ct as deserializeTypedArrays, d as registerEventBusService, dt as CustomActionRegistry, et as createAddonService, f as AddonDepsManager, ft as CapabilityHandle, g as CapUsageRegistry, h as createHwAccelService, ht as resolveAddonClass, i as createUdsAddonContext, it as capActionSuffix, j as AGENT_CAP_FWD_ACTION, k as createUdsLogger, l as EVENT_TOPIC_PREFIX, lt as serializeTypedArrays, m as resolveHwAccel, mt as installManifestNativeDeps, n as adaptBrokerToCluster, nt as NATIVE_PROVIDER_SERVICE_INFIX, o as getOrInitReadinessRegistry, ot as capServiceName, p as createKernelHwAccel, pt as CapabilityUnavailableError, q as buildNativeCapProxy, r as createAddonContext, rt as capActionName, s as getOrInitReadinessRegistryForClient, st as parseCapAction, t as installManifestPythonDeps, tt as validateProviderRegistrations, u as getBrokerEventBus, ut as DeviceRegistry, v as getCapUsageRegistry, w as localProviderLink, x as buildLinkChain, y as brokerCallForCap, z as UDS_NO_ROUTE_PREFIX } from "./manifest-python-deps-CXbKrOdk.mjs";
22
+ import { A as createUdsLoggerWithControl, B as createLocalTransport, C as ipcParentLink, D as createUdsEventBus, E as createUdsEventBridge, F as CapRouteError, G as FrameDecoder, H as UdsLocalTransportServer, I as classifyCapRoute, J as buildUdsNativeCapProxy, K as encodeFrame, L as LocalChildClient, M as AGENT_CAP_FWD_SERVICE, N as CapRouteResolver, O as udsChildLogToWorkerEntry, P as callWithServiceDiscovery, Q as mountNativeCapService, R as LocalChildRegistry, S as ipcChildLink, T as createParentUnownedCallHandler, U as SocketChannel, V as UdsLocalTransportClient, W as localEndpointPath, Y as createBrokerDeviceManagerApi, _ as __resetCapUsageRegistryForTests, a as getWorkerDeviceRegistry, at as capBareAction, b as brokerTransportLink, c as setHubConnected, ct as deserializeTypedArrays, d as registerEventBusService, dt as CapabilityHandle, et as createAddonService, f as AddonDepsManager, ft as CapabilityUnavailableError, g as CapUsageRegistry, h as createHwAccelService, i as createUdsAddonContext, it as capActionSuffix, j as AGENT_CAP_FWD_ACTION, k as createUdsLogger, l as EVENT_TOPIC_PREFIX, lt as serializeTypedArrays, m as resolveHwAccel, mt as resolveAddonClass, n as adaptBrokerToCluster, nt as NATIVE_PROVIDER_SERVICE_INFIX, o as getOrInitReadinessRegistry, ot as capServiceName, p as createKernelHwAccel, pt as installManifestNativeDeps, q as buildNativeCapProxy, r as createAddonContext, rt as capActionName, s as getOrInitReadinessRegistryForClient, st as parseCapAction, t as installManifestPythonDeps, tt as validateProviderRegistrations, u as getBrokerEventBus, ut as DeviceRegistry, v as getCapUsageRegistry, w as localProviderLink, x as buildLinkChain, y as brokerCallForCap, z as UDS_NO_ROUTE_PREFIX } from "./manifest-python-deps-CoJXeb9u.mjs";
23
+ import { t as CustomActionRegistry } from "./custom-action-registry-BEXwC-oo.mjs";
22
24
  import { n as require_src, t as require_graceful_fs } from "./graceful-fs-BoR9GuPS.mjs";
23
25
  import { PYTHON_VERSION, buildBinaryPath, downloadBinary, ensureBinary, ensureFfmpeg, ensurePython, findInPath, getFfmpegDownloadUrl, getPlatformInfo, getPythonDownloadUrl, installPythonPackages, installPythonRequirements } from "@camstack/types/node";
24
- import { createServer, request } from "node:http";
26
+ import { request } from "node:http";
25
27
  import * as fs$17 from "node:fs";
26
- import { accessSync, constants, createReadStream, existsSync, mkdirSync, promises, readFileSync } from "node:fs";
28
+ import { accessSync, constants, existsSync, mkdirSync, readFileSync } from "node:fs";
27
29
  import * as path$40 from "node:path";
28
- import path, { dirname, isAbsolute, join, posix, resolve, win32 } from "node:path";
30
+ import { dirname, isAbsolute, join, posix, resolve, win32 } from "node:path";
29
31
  import { DATAPLANE_SECRET_HEADER, EventCategory, RUNTIME_DEFAULTS, ReadinessRegistry, ReadinessTimeoutError, asJsonObject, asNumber, asString, createEvent, emitDownForOwnedCaps, errMsg, lifecycleJobSchema, parseJsonObject, parseJsonUnknown, readinessKey, scopeKey } from "@camstack/types";
30
32
  import { X509Certificate, createHash, randomUUID, timingSafeEqual } from "node:crypto";
31
33
  import { execFile, execFileSync, spawn } from "node:child_process";
@@ -35,6 +37,7 @@ import * as vm from "node:vm";
35
37
  import * as os$17 from "node:os";
36
38
  import { lstat, readdir, readlink, realpath } from "node:fs/promises";
37
39
  import { z } from "zod";
40
+ import { asJsonObject as asJsonObject$1, parseJsonUnknown as parseJsonUnknown$1 } from "@camstack/types/addon";
38
41
  import { lstatSync, readdir as readdir$1, readdirSync, readlinkSync, realpathSync } from "fs";
39
42
  import { EventEmitter } from "node:events";
40
43
  import Pe from "node:stream";
@@ -73,216 +76,6 @@ var EventBus = class {
73
76
  }
74
77
  };
75
78
  //#endregion
76
- //#region src/http/authenticated-file-server.ts
77
- /**
78
- * Authenticated data-plane file-server — a shared host primitive for addons that
79
- * must serve files (media segments, scrub-frame stores, exported clips) directly
80
- * to browsers/players, node-direct (NOT through the hub tRPC, per the recording
81
- * design's "no media bytes traverse the hub").
82
- *
83
- * It bundles the three things every such server needs so addons don't re-roll
84
- * them: CORS (the data plane is cross-origin from the admin-ui — a native
85
- * `<video src>` is exempt but hls.js / WebCodecs fetch via XHR and are blocked
86
- * without it), HTTP `Range` (seeking / partial fetch), and a SCOPED TOKEN check.
87
- *
88
- * The token rides as the FIRST path segment (`<urlPrefix><token>/<relPath>`) so
89
- * a player resolving a manifest's RELATIVE child URIs carries it on every
90
- * sub-request automatically — no per-URI rewriting, no cookies/headers a dumb
91
- * player can't set. The caller supplies `verifyToken(token, relPath)`; the
92
- * primitive stays auth-scheme-agnostic.
93
- *
94
- * Pure helpers (`parseTokenizedUrl`, `resolveFilePath`, `contentTypeFor`,
95
- * `parseRangeHeader`) are exported for unit testing; `createAuthenticatedFileServer`
96
- * is the thin `node:http` glue.
97
- */
98
- function corsHeaders(origin) {
99
- return {
100
- "access-control-allow-origin": origin,
101
- "access-control-allow-methods": "GET, HEAD, OPTIONS",
102
- "access-control-allow-headers": "range",
103
- "access-control-expose-headers": "content-length, content-range, accept-ranges",
104
- "access-control-max-age": "86400"
105
- };
106
- }
107
- /**
108
- * Split a request URL (`<urlPrefix><token>/<relPath>`) into its token + rel
109
- * path. Returns null for anything outside the prefix or missing either part.
110
- * `relPath` is URL-decoded; a decode failure → null.
111
- */
112
- function parseTokenizedUrl(urlPrefix, urlPath) {
113
- if (!urlPath.startsWith(urlPrefix)) return null;
114
- const after = urlPath.slice(urlPrefix.length);
115
- const slash = after.indexOf("/");
116
- if (slash <= 0) return null;
117
- const token = after.slice(0, slash);
118
- let rel;
119
- try {
120
- rel = decodeURIComponent(after.slice(slash + 1));
121
- } catch {
122
- return null;
123
- }
124
- if (rel.length === 0) return null;
125
- return {
126
- token,
127
- rel
128
- };
129
- }
130
- /**
131
- * Map a rel path to one candidate absolute path PER root, keeping only roots the
132
- * path stays within (traversal guard). The handler serves the first candidate
133
- * that exists. Rejects a path that escapes every root.
134
- */
135
- function resolveFilePath(roots, rel) {
136
- if (rel.length === 0) return { error: "forbidden" };
137
- const candidates = [];
138
- for (const rootDir of roots) {
139
- const root = path.resolve(rootDir);
140
- const abs = path.resolve(root, rel);
141
- if (abs === root || abs.startsWith(root + path.sep)) candidates.push(abs);
142
- }
143
- if (candidates.length === 0) return { error: "forbidden" };
144
- return { candidates };
145
- }
146
- var DEFAULT_CONTENT_TYPES = {
147
- ".m3u8": "application/vnd.apple.mpegurl",
148
- ".m4s": "video/mp4",
149
- ".mp4": "video/mp4",
150
- ".idx": "application/octet-stream",
151
- ".html": "text/html; charset=utf-8",
152
- ".js": "application/javascript; charset=utf-8",
153
- ".mjs": "application/javascript; charset=utf-8",
154
- ".css": "text/css; charset=utf-8",
155
- ".json": "application/json; charset=utf-8",
156
- ".map": "application/json; charset=utf-8",
157
- ".svg": "image/svg+xml",
158
- ".png": "image/png",
159
- ".jpg": "image/jpeg",
160
- ".jpeg": "image/jpeg",
161
- ".ico": "image/x-icon",
162
- ".woff2": "font/woff2",
163
- ".woff": "font/woff",
164
- ".ttf": "font/ttf"
165
- };
166
- function contentTypeFor(filePath, overrides) {
167
- const ext = path.extname(filePath).toLowerCase();
168
- return overrides?.[ext] ?? DEFAULT_CONTENT_TYPES[ext] ?? "application/octet-stream";
169
- }
170
- /** Parse an HTTP `Range` header against a known size. Null = serve whole file. */
171
- function parseRangeHeader(header, size) {
172
- if (!header) return null;
173
- const m = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
174
- if (!m) return null;
175
- const [, rawStart, rawEnd] = m;
176
- if (rawStart === "" && rawEnd === "") return null;
177
- let start;
178
- let end;
179
- if (rawStart === "") {
180
- const n = Number(rawEnd);
181
- if (n <= 0) return null;
182
- start = Math.max(0, size - n);
183
- end = size - 1;
184
- } else {
185
- start = Number(rawStart);
186
- end = rawEnd === "" ? size - 1 : Number(rawEnd);
187
- }
188
- if (!Number.isFinite(start) || !Number.isFinite(end) || start > end || start >= size) return null;
189
- return {
190
- start,
191
- end: Math.min(end, size - 1)
192
- };
193
- }
194
- /**
195
- * Start an authenticated file-server. Returns the bound port so the caller can
196
- * build URLs. `getRoots`/`verifyToken` are invoked per request, so they always
197
- * reflect the live config.
198
- */
199
- async function createAuthenticatedFileServer(opts) {
200
- const cors = corsHeaders(opts.corsOrigin ?? "*");
201
- const server = createServer((req, res) => {
202
- handle(opts, cors, req, res);
203
- });
204
- await new Promise((resolve, reject) => {
205
- server.once("error", reject);
206
- server.listen(opts.port, () => resolve());
207
- });
208
- const addr = server.address();
209
- return {
210
- port: typeof addr === "object" && addr ? addr.port : opts.port,
211
- close: () => new Promise((resolve) => server.close(() => resolve()))
212
- };
213
- }
214
- async function handle(opts, cors, req, res) {
215
- if (req.method === "OPTIONS") {
216
- res.writeHead(204, cors).end();
217
- return;
218
- }
219
- if (req.method !== "GET" && req.method !== "HEAD") {
220
- res.writeHead(405, cors).end();
221
- return;
222
- }
223
- const urlPath = (req.url ?? "").split("?")[0] ?? "";
224
- const parsed = parseTokenizedUrl(opts.urlPrefix, urlPath);
225
- if (!parsed) {
226
- res.writeHead(403, cors).end();
227
- return;
228
- }
229
- if (!opts.verifyToken(parsed.token, parsed.rel)) {
230
- res.writeHead(401, cors).end();
231
- return;
232
- }
233
- const resolved = resolveFilePath(opts.getRoots(), parsed.rel);
234
- if ("error" in resolved) {
235
- res.writeHead(403, cors).end();
236
- return;
237
- }
238
- let absPath = null;
239
- let size = 0;
240
- for (const candidate of resolved.candidates) try {
241
- const st = await promises.stat(candidate);
242
- if (st.isFile()) {
243
- absPath = candidate;
244
- size = st.size;
245
- break;
246
- }
247
- } catch {}
248
- if (absPath === null) {
249
- res.writeHead(404, cors).end();
250
- return;
251
- }
252
- const baseHeaders = {
253
- ...cors,
254
- "content-type": contentTypeFor(absPath, opts.contentTypes),
255
- "accept-ranges": "bytes",
256
- "cache-control": "no-cache"
257
- };
258
- const range = parseRangeHeader(req.headers.range, size);
259
- if (range) {
260
- res.writeHead(206, {
261
- ...baseHeaders,
262
- "content-range": `bytes ${range.start}-${range.end}/${size}`,
263
- "content-length": String(range.end - range.start + 1)
264
- });
265
- if (req.method === "HEAD") {
266
- res.end();
267
- return;
268
- }
269
- createReadStream(absPath, {
270
- start: range.start,
271
- end: range.end
272
- }).pipe(res);
273
- return;
274
- }
275
- res.writeHead(200, {
276
- ...baseHeaders,
277
- "content-length": String(size)
278
- });
279
- if (req.method === "HEAD") {
280
- res.end();
281
- return;
282
- }
283
- createReadStream(absPath).pipe(res);
284
- }
285
- //#endregion
286
79
  //#region src/http/data-plane-registry.ts
287
80
  function trimSlashes(s) {
288
81
  return s.replace(/^\/+/, "").replace(/\/+$/, "");
@@ -383,457 +176,6 @@ function proxyToUpstream(opts) {
383
176
  clientReq.pipe(upstream);
384
177
  }
385
178
  //#endregion
386
- //#region src/http/file-data-plane.ts
387
- /**
388
- * A ready-made data-plane request handler that serves files from a set of roots
389
- * with HTTP `Range`, for addons whose data-plane is "stream files off disk"
390
- * (recording playback, scrub-frame stores, exported clips).
391
- *
392
- * This is the addon-side handler the addon hands to `ctx.dataPlane.serve({ handler })`.
393
- * It receives the REAL Node `req`/`res`, resolves the request path against the
394
- * addon's roots (traversal-guarded), and streams the file. NO token and NO CORS:
395
- * the hub already authenticated the caller and the data plane is same-origin
396
- * through the hub's port. Reuses the shared Range/content-type/traversal helpers
397
- * so there is one implementation across the standalone file-server and this.
398
- */
399
- /** Build a `(req, res)` handler that serves `getRoots()` files with Range. */
400
- function createFileDataPlaneHandler(opts) {
401
- return async (req, res) => {
402
- if (req.method !== "GET" && req.method !== "HEAD") {
403
- res.writeHead(405).end();
404
- return;
405
- }
406
- const urlPath = (req.url ?? "/").split("?")[0] ?? "/";
407
- let rel;
408
- try {
409
- rel = decodeURIComponent(urlPath.replace(/^\/+/, ""));
410
- } catch {
411
- res.writeHead(400).end();
412
- return;
413
- }
414
- if (rel.length === 0) {
415
- res.writeHead(404).end();
416
- return;
417
- }
418
- const resolved = resolveFilePath(opts.getRoots(), rel);
419
- if ("error" in resolved) {
420
- res.writeHead(403).end();
421
- return;
422
- }
423
- let absPath = null;
424
- let size = 0;
425
- for (const candidate of resolved.candidates) try {
426
- const st = await promises.stat(candidate);
427
- if (st.isFile()) {
428
- absPath = candidate;
429
- size = st.size;
430
- break;
431
- }
432
- } catch {}
433
- if (absPath === null) {
434
- res.writeHead(404).end();
435
- return;
436
- }
437
- const baseHeaders = {
438
- "content-type": contentTypeFor(absPath, opts.contentTypes),
439
- "accept-ranges": "bytes",
440
- "cache-control": "no-cache"
441
- };
442
- const range = parseRangeHeader(req.headers.range, size);
443
- if (range) {
444
- res.writeHead(206, {
445
- ...baseHeaders,
446
- "content-range": `bytes ${range.start}-${range.end}/${size}`,
447
- "content-length": String(range.end - range.start + 1)
448
- });
449
- if (req.method === "HEAD") {
450
- res.end();
451
- return;
452
- }
453
- createReadStream(absPath, {
454
- start: range.start,
455
- end: range.end
456
- }).pipe(res);
457
- return;
458
- }
459
- res.writeHead(200, {
460
- ...baseHeaders,
461
- "content-length": String(size)
462
- });
463
- if (req.method === "HEAD") {
464
- res.end();
465
- return;
466
- }
467
- createReadStream(absPath).pipe(res);
468
- };
469
- }
470
- //#endregion
471
- //#region src/download/model-downloader.ts
472
- /** Build fetch headers, including HF auth token for huggingface.co URLs */
473
- function buildHeaders(url) {
474
- const headers = { "User-Agent": "CamStack/1.0" };
475
- const hfToken = process.env["HF_TOKEN"] ?? process.env["HUGGING_FACE_HUB_TOKEN"];
476
- if (hfToken && url.includes("huggingface.co")) headers["Authorization"] = `Bearer ${hfToken}`;
477
- return headers;
478
- }
479
- /**
480
- * Download a single file from a URL to a destination path.
481
- * Uses native fetch() (Node 22+) which handles redirects natively.
482
- * Streams to disk with optional progress callback.
483
- * Returns the destination path. Skips download if file already exists.
484
- */
485
- async function downloadFile(url, destPath, onProgress) {
486
- if (fs$17.existsSync(destPath)) return destPath;
487
- fs$17.mkdirSync(path$40.dirname(destPath), { recursive: true });
488
- const tmpPath = destPath + ".downloading";
489
- try {
490
- const response = await fetch(url, {
491
- redirect: "follow",
492
- headers: buildHeaders(url)
493
- });
494
- if (!response.ok) throw new Error(`HTTP ${response.status} downloading ${url}`);
495
- if (!response.body) throw new Error(`No response body from ${url}`);
496
- const total = parseInt(response.headers.get("content-length") ?? "0", 10);
497
- let downloaded = 0;
498
- const fileStream = fs$17.createWriteStream(tmpPath);
499
- const reader = response.body.getReader();
500
- try {
501
- for (;;) {
502
- const { done, value } = await reader.read();
503
- if (done || !value) break;
504
- fileStream.write(value);
505
- downloaded += value.length;
506
- onProgress?.(downloaded, total);
507
- }
508
- } finally {
509
- fileStream.end();
510
- await new Promise((resolve, reject) => {
511
- fileStream.on("finish", resolve);
512
- fileStream.on("error", reject);
513
- });
514
- }
515
- fs$17.renameSync(tmpPath, destPath);
516
- return destPath;
517
- } catch (err) {
518
- try {
519
- fs$17.unlinkSync(tmpPath);
520
- } catch {}
521
- throw err;
522
- }
523
- }
524
- /**
525
- * Fetch JSON from a URL using native fetch().
526
- */
527
- async function fetchJson(url) {
528
- const response = await fetch(url, {
529
- redirect: "follow",
530
- headers: buildHeaders(url)
531
- });
532
- if (!response.ok) throw new Error(`HTTP ${response.status} fetching ${url}`);
533
- return response.json();
534
- }
535
- /**
536
- * Download a model with fallback URLs and optional SHA256 verification.
537
- * Legacy API preserved for backward compatibility -- delegates to downloadFile().
538
- */
539
- async function downloadModel(options) {
540
- const { url, fallbackUrls = [], destDir, filename, expectedSha256, onProgress } = options;
541
- const fname = filename ?? url.split("/").pop() ?? "model.bin";
542
- const destPath = path$40.join(destDir, fname);
543
- if (fs$17.existsSync(destPath)) return {
544
- filePath: destPath,
545
- downloadedBytes: 0,
546
- fromCache: true
547
- };
548
- fs$17.mkdirSync(destDir, { recursive: true });
549
- const urls = [url, ...fallbackUrls];
550
- let lastError = null;
551
- for (const tryUrl of urls) try {
552
- await downloadFile(tryUrl, destPath, onProgress);
553
- if (expectedSha256) {
554
- const hash = await computeSha256(destPath);
555
- if (hash !== expectedSha256) {
556
- fs$17.unlinkSync(destPath);
557
- throw new Error(`SHA256 mismatch: expected ${expectedSha256}, got ${hash}`);
558
- }
559
- }
560
- return {
561
- filePath: destPath,
562
- downloadedBytes: fs$17.statSync(destPath).size,
563
- fromCache: false
564
- };
565
- } catch (e) {
566
- lastError = e;
567
- if (fs$17.existsSync(destPath)) fs$17.unlinkSync(destPath);
568
- }
569
- throw lastError ?? /* @__PURE__ */ new Error(`Failed to download model from ${url}`);
570
- }
571
- async function computeSha256(filePath) {
572
- return new Promise((resolve, reject) => {
573
- const hash = createHash("sha256");
574
- const stream = fs$17.createReadStream(filePath);
575
- stream.on("data", (chunk) => hash.update(chunk));
576
- stream.on("end", () => resolve(hash.digest("hex")));
577
- stream.on("error", reject);
578
- });
579
- }
580
- /**
581
- * Download every file in a HuggingFace directory bundle (e.g.,
582
- * `.mlpackage` / OpenVINO IR pair) atomically. `knownFiles` lists the
583
- * relative paths inside the directory; the function fetches each from
584
- * `${url}/${file}` and renames the staging directory only on full
585
- * success. Mirrors `ModelDownloadService.downloadDirectory` but
586
- * exposed as a standalone for catalog-less callers.
587
- */
588
- async function downloadDirectory(url, destDir, knownFiles, onProgress) {
589
- const match = url.match(/huggingface\.co\/([^/]+\/[^/]+)\/resolve\/main\/(.+)/);
590
- if (!match) throw new Error(`Cannot parse HuggingFace URL: ${url}`);
591
- const [, repo, dirPath] = match;
592
- const files = (knownFiles ?? []).map((f) => ({
593
- relativePath: f,
594
- fileUrl: `https://huggingface.co/${repo}/resolve/main/${dirPath}/${f}`
595
- }));
596
- if (files.length === 0) throw new Error(`Directory bundle requires explicit \`files\` list (got none for ${url})`);
597
- const tmpDir = destDir + ".downloading";
598
- fs$17.rmSync(tmpDir, {
599
- recursive: true,
600
- force: true
601
- });
602
- fs$17.mkdirSync(tmpDir, { recursive: true });
603
- let totalDownloaded = 0;
604
- try {
605
- for (const file of files) {
606
- const destPath = path$40.join(tmpDir, file.relativePath);
607
- fs$17.mkdirSync(path$40.dirname(destPath), { recursive: true });
608
- await downloadFile(file.fileUrl, destPath, (downloaded, _total) => {
609
- onProgress?.(totalDownloaded + downloaded, void 0);
610
- });
611
- totalDownloaded += fs$17.statSync(destPath).size;
612
- }
613
- fs$17.rmSync(destDir, {
614
- recursive: true,
615
- force: true
616
- });
617
- fs$17.renameSync(tmpDir, destDir);
618
- } catch (err) {
619
- fs$17.rmSync(tmpDir, {
620
- recursive: true,
621
- force: true
622
- });
623
- throw err;
624
- }
625
- }
626
- /**
627
- * Resolve a `ModelCatalogEntry` against `modelsDir`: download model file
628
- * (or directory bundle) + extra files (labels JSON, charset dict, …),
629
- * skip if already on disk. Returns the local model path.
630
- */
631
- async function ensureModel(modelsDir, entry, format, onProgress) {
632
- const formatEntry = entry.formats[format];
633
- if (!formatEntry) throw new Error(`Model "${entry.id}" has no ${format} format. Available: ${Object.keys(entry.formats).join(", ")}`);
634
- if (entry.extraFiles) for (const extra of entry.extraFiles) await downloadFile(extra.url, path$40.join(modelsDir, extra.filename));
635
- const filename = formatEntry.url.split("/").pop() ?? `${entry.id}.${format}`;
636
- const modelPath = path$40.join(modelsDir, filename);
637
- if (fs$17.existsSync(modelPath)) if (formatEntry.isDirectory && !fs$17.existsSync(path$40.join(modelPath, "Manifest.json"))) fs$17.rmSync(modelPath, {
638
- recursive: true,
639
- force: true
640
- });
641
- else return modelPath;
642
- fs$17.mkdirSync(modelsDir, { recursive: true });
643
- if (formatEntry.isDirectory) await downloadDirectory(formatEntry.url, modelPath, formatEntry.files, onProgress);
644
- else await downloadFile(formatEntry.url, modelPath, (downloaded, total) => onProgress?.(downloaded, total === 0 ? void 0 : total));
645
- return modelPath;
646
- }
647
- /** Compute the on-disk path for a given model + format, even when not yet downloaded. */
648
- function getModelFilePath(modelsDir, entry, format) {
649
- const formatEntry = entry.formats[format];
650
- if (!formatEntry) return null;
651
- const filename = formatEntry.url.split("/").pop() ?? `${entry.id}.${format}`;
652
- return path$40.join(modelsDir, filename);
653
- }
654
- /** True iff the model file (or `Manifest.json` for directory bundles) exists and is non-empty. */
655
- function isModelDownloaded(modelsDir, entry, format) {
656
- const formatEntry = entry.formats[format];
657
- if (!formatEntry) return false;
658
- const modelPath = getModelFilePath(modelsDir, entry, format);
659
- if (!modelPath || !fs$17.existsSync(modelPath)) return false;
660
- if (formatEntry.isDirectory) return fs$17.existsSync(path$40.join(modelPath, "Manifest.json"));
661
- return fs$17.statSync(modelPath).size > 0;
662
- }
663
- /** Remove the on-disk model file/directory. Returns true if something was deleted. */
664
- function deleteModelFromDisk(modelsDir, entry, format) {
665
- const modelPath = getModelFilePath(modelsDir, entry, format);
666
- if (!modelPath || !fs$17.existsSync(modelPath)) return false;
667
- if (entry.formats[format]?.isDirectory) fs$17.rmSync(modelPath, {
668
- recursive: true,
669
- force: true
670
- });
671
- else fs$17.unlinkSync(modelPath);
672
- return true;
673
- }
674
- //#endregion
675
- //#region src/download/model-download-service.ts
676
- /**
677
- * Unified model download service.
678
- *
679
- * Handles downloading model files and extra files (labels, dicts) from a
680
- * catalog of ModelCatalogEntry items. Supports single-file models and
681
- * directory bundles (e.g., .mlpackage for CoreML).
682
- *
683
- * Addons use this via `context.models.ensure(modelId, format)`.
684
- */
685
- var ModelDownloadService = class {
686
- modelsDir;
687
- onProgress;
688
- catalog;
689
- constructor(modelsDir, catalog, onProgress) {
690
- this.modelsDir = modelsDir;
691
- this.onProgress = onProgress;
692
- const map = /* @__PURE__ */ new Map();
693
- for (const entry of catalog) map.set(entry.id, entry);
694
- this.catalog = map;
695
- }
696
- /**
697
- * Ensure a model (and its extra files) is downloaded.
698
- * Returns the local filesystem path to the model file/directory.
699
- */
700
- async ensure(modelId, format) {
701
- const entry = this.catalog.get(modelId);
702
- if (!entry) throw new Error(`ModelDownloadService: unknown model "${modelId}"`);
703
- const selectedFormat = format ?? this.pickDefaultFormat(entry);
704
- const formatEntry = entry.formats[selectedFormat];
705
- if (!formatEntry) throw new Error(`ModelDownloadService: model "${modelId}" has no ${selectedFormat} format`);
706
- await this.ensureExtraFiles(modelId);
707
- const modelPath = this.modelFilePath(entry, selectedFormat);
708
- if (fs$17.existsSync(modelPath)) if (formatEntry.isDirectory) if (!fs$17.existsSync(path$40.join(modelPath, "Manifest.json"))) fs$17.rmSync(modelPath, {
709
- recursive: true,
710
- force: true
711
- });
712
- else return modelPath;
713
- else return modelPath;
714
- fs$17.mkdirSync(this.modelsDir, { recursive: true });
715
- if (formatEntry.isDirectory) await this.downloadDirectory(formatEntry.url, modelPath, formatEntry.files, modelId);
716
- else await downloadFile(formatEntry.url, modelPath, this.onProgress ? (downloaded, total) => this.onProgress(modelId, downloaded, total) : void 0);
717
- return modelPath;
718
- }
719
- /**
720
- * Ensure extra files for a model are downloaded.
721
- * Returns the local paths of all extra files.
722
- */
723
- async ensureExtraFiles(modelId) {
724
- const entry = this.catalog.get(modelId);
725
- if (!entry) throw new Error(`ModelDownloadService: unknown model "${modelId}"`);
726
- const extras = entry.extraFiles;
727
- if (!extras || extras.length === 0) return [];
728
- const paths = [];
729
- for (const extra of extras) {
730
- const destPath = path$40.join(this.modelsDir, extra.filename);
731
- await downloadFile(extra.url, destPath);
732
- paths.push(destPath);
733
- }
734
- return paths;
735
- }
736
- /** Absolute path to the shared models directory. */
737
- getModelsDir() {
738
- return this.modelsDir;
739
- }
740
- /** Check if a model file is already present on disk. */
741
- isDownloaded(modelId, format) {
742
- const entry = this.catalog.get(modelId);
743
- if (!entry) return false;
744
- const selectedFormat = format ?? this.pickDefaultFormat(entry);
745
- const formatEntry = entry.formats[selectedFormat];
746
- if (!formatEntry) return false;
747
- const modelPath = this.modelFilePath(entry, selectedFormat);
748
- if (!fs$17.existsSync(modelPath)) return false;
749
- if (formatEntry.isDirectory) return fs$17.existsSync(path$40.join(modelPath, "Manifest.json"));
750
- return fs$17.statSync(modelPath).size > 0;
751
- }
752
- /** Get the catalog entry for a model by ID. */
753
- getEntry(modelId) {
754
- return this.catalog.get(modelId);
755
- }
756
- pickDefaultFormat(entry) {
757
- for (const fmt of [
758
- "onnx",
759
- "coreml",
760
- "openvino",
761
- "tflite",
762
- "pt"
763
- ]) if (entry.formats[fmt]) return fmt;
764
- const first = Object.keys(entry.formats)[0];
765
- if (first) return first;
766
- throw new Error(`ModelDownloadService: model "${entry.id}" has no formats`);
767
- }
768
- modelFilePath(entry, format) {
769
- const formatEntry = entry.formats[format];
770
- if (!formatEntry) throw new Error(`Model ${entry.id} has no ${format} format`);
771
- const urlParts = formatEntry.url.split("/");
772
- const filename = urlParts[urlParts.length - 1] ?? `${entry.id}.${format}`;
773
- return path$40.join(this.modelsDir, filename);
774
- }
775
- /**
776
- * Download a directory bundle (e.g., .mlpackage) from HuggingFace.
777
- * ATOMIC: downloads to temp dir, renames only on complete success.
778
- */
779
- async downloadDirectory(url, destDir, knownFiles, _modelId) {
780
- const match = url.match(/huggingface\.co\/([^/]+\/[^/]+)\/resolve\/main\/(.+)/);
781
- if (!match) throw new Error(`Cannot parse HuggingFace URL: ${url}`);
782
- const [, repo, dirPath] = match;
783
- let filesToDownload;
784
- if (knownFiles && knownFiles.length > 0) filesToDownload = knownFiles.map((f) => ({
785
- relativePath: f,
786
- fileUrl: `https://huggingface.co/${repo}/resolve/main/${dirPath}/${f}`
787
- }));
788
- else {
789
- const hfFiles = await this.listHfFiles(repo, dirPath);
790
- if (hfFiles.length === 0) throw new Error(`No files found in HuggingFace directory: ${dirPath}`);
791
- filesToDownload = hfFiles.map((f) => ({
792
- relativePath: f.path.substring(dirPath.length + 1),
793
- fileUrl: `https://huggingface.co/${repo}/resolve/main/${f.path}`
794
- }));
795
- }
796
- const tmpDir = destDir + ".downloading";
797
- fs$17.rmSync(tmpDir, {
798
- recursive: true,
799
- force: true
800
- });
801
- fs$17.mkdirSync(tmpDir, { recursive: true });
802
- try {
803
- for (const file of filesToDownload) {
804
- const destPath = path$40.join(tmpDir, file.relativePath);
805
- fs$17.mkdirSync(path$40.dirname(destPath), { recursive: true });
806
- await downloadFile(file.fileUrl, destPath);
807
- }
808
- fs$17.rmSync(destDir, {
809
- recursive: true,
810
- force: true
811
- });
812
- fs$17.renameSync(tmpDir, destDir);
813
- } catch (err) {
814
- fs$17.rmSync(tmpDir, {
815
- recursive: true,
816
- force: true
817
- });
818
- throw err;
819
- }
820
- }
821
- /** Recursively list all files in a HuggingFace directory via API. */
822
- async listHfFiles(repo, dirPath) {
823
- const entries = await fetchJson(`https://huggingface.co/api/models/${repo}/tree/main/${dirPath}`);
824
- const files = [];
825
- for (const entry of entries) if (entry.type === "file") files.push({
826
- path: entry.path,
827
- size: entry.size ?? entry.lfs?.size ?? 0
828
- });
829
- else if (entry.type === "directory") {
830
- const subFiles = await this.listHfFiles(repo, entry.path);
831
- files.push(...subFiles);
832
- }
833
- return files;
834
- }
835
- };
836
- //#endregion
837
179
  //#region src/python/python-env-manager.ts
838
180
  var execFileAsync$2 = promisify(execFile);
839
181
  var PythonEnvManager = class {
@@ -8025,7 +7367,7 @@ var ConfigManager = class ConfigManager {
8025
7367
  constructor(configPath) {
8026
7368
  this.configPath = configPath;
8027
7369
  const rawYaml = this.loadYaml();
8028
- const merged = this.applyEnvOverrides(asJsonObject(rawYaml) ?? {});
7370
+ const merged = this.applyEnvOverrides(asJsonObject$1(rawYaml) ?? {});
8029
7371
  this.bootstrapConfig = bootstrapSchema.parse(merged);
8030
7372
  this.warnDefaultCredentials();
8031
7373
  const dataPath = this.bootstrapConfig.server.dataPath ?? "camstack-data";
@@ -8080,9 +7422,9 @@ var ConfigManager = class ConfigManager {
8080
7422
  */
8081
7423
  getSection(section) {
8082
7424
  const merged = {};
8083
- const defaults = asJsonObject(this.getFromRuntimeDefaults(section));
7425
+ const defaults = asJsonObject$1(this.getFromRuntimeDefaults(section));
8084
7426
  if (defaults) Object.assign(merged, defaults);
8085
- const bootstrapSection = asJsonObject({ ...this.bootstrapConfig }[section]);
7427
+ const bootstrapSection = asJsonObject$1({ ...this.bootstrapConfig }[section]);
8086
7428
  if (bootstrapSection) Object.assign(merged, bootstrapSection);
8087
7429
  if (this.settingsStore !== null) {
8088
7430
  const nested = this.getNestedFromSystemSettings(section);
@@ -8101,7 +7443,7 @@ var ConfigManager = class ConfigManager {
8101
7443
  /** Read all config for an addon from addon_settings. */
8102
7444
  getAddonConfig(addonId) {
8103
7445
  if (this.settingsStore !== null) return this.settingsStore.getAllAddon(addonId);
8104
- return asJsonObject(this.getFromBootstrap(`addons.${addonId}`)) ?? {};
7446
+ return asJsonObject$1(this.getFromBootstrap(`addons.${addonId}`)) ?? {};
8105
7447
  }
8106
7448
  /** Write (bulk-replace) config for an addon to addon_settings. */
8107
7449
  setAddonConfig(addonId, config) {
@@ -8339,8 +7681,8 @@ var ConfigManager = class ConfigManager {
8339
7681
  update(section, data) {
8340
7682
  if (!ConfigManager.BOOTSTRAP_SECTIONS.has(section)) throw new Error(`[ConfigManager] Section "${section}" is a runtime setting — use setSection() to persist via the settings-store, not update() which writes to config.yaml`);
8341
7683
  let raw = {};
8342
- if (fs$17.existsSync(this.configPath)) raw = asJsonObject(load$1(fs$17.readFileSync(this.configPath, "utf-8"))) ?? {};
8343
- const existing = asJsonObject(raw[section]) ?? {};
7684
+ if (fs$17.existsSync(this.configPath)) raw = asJsonObject$1(load$1(fs$17.readFileSync(this.configPath, "utf-8"))) ?? {};
7685
+ const existing = asJsonObject$1(raw[section]) ?? {};
8344
7686
  raw[section] = {
8345
7687
  ...existing,
8346
7688
  ...data
@@ -8441,13 +7783,13 @@ var ConfigManager = class ConfigManager {
8441
7783
  loadRuntimeState() {
8442
7784
  if (!fs$17.existsSync(this.runtimeStatePath)) return EMPTY_RUNTIME_STATE;
8443
7785
  try {
8444
- const parsed = asJsonObject(parseJsonUnknown(fs$17.readFileSync(this.runtimeStatePath, "utf-8")));
7786
+ const parsed = asJsonObject$1(parseJsonUnknown$1(fs$17.readFileSync(this.runtimeStatePath, "utf-8")));
8445
7787
  if (parsed === null) return EMPTY_RUNTIME_STATE;
8446
- const systemActivation = asJsonObject(parsed.systemActivation) ?? {};
8447
- const deviceActivationRaw = asJsonObject(parsed.deviceActivation) ?? {};
7788
+ const systemActivation = asJsonObject$1(parsed.systemActivation) ?? {};
7789
+ const deviceActivationRaw = asJsonObject$1(parsed.deviceActivation) ?? {};
8448
7790
  const deviceActivation = {};
8449
7791
  for (const [deviceId, entry] of Object.entries(deviceActivationRaw)) {
8450
- const nested = asJsonObject(entry);
7792
+ const nested = asJsonObject$1(entry);
8451
7793
  if (nested === null) continue;
8452
7794
  const bools = {};
8453
7795
  for (const [k, v] of Object.entries(nested)) if (typeof v === "boolean") bools[k] = v;