@camstack/system 1.0.6 → 1.0.8
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 +40 -23
- package/dist/addon-runner.mjs +20 -4
- package/dist/addon-utils.d.ts +20 -0
- package/dist/addon-utils.js +11 -0
- package/dist/addon-utils.mjs +3 -0
- package/dist/builtins/device-manager/device-manager.addon.js +8 -8
- package/dist/builtins/device-manager/device-manager.addon.mjs +8 -8
- package/dist/builtins/native-metrics/native-metrics.addon.d.ts +8 -0
- package/dist/builtins/native-metrics/native-metrics.addon.js +50 -3
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +50 -3
- package/dist/builtins/platform-probe/index.js +27 -139
- package/dist/builtins/platform-probe/index.mjs +28 -140
- package/dist/builtins/platform-probe/platform-scorer.d.ts +17 -10
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +2 -2
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +2 -2
- package/dist/custom-action-registry-BEXwC-oo.mjs +38 -0
- package/dist/custom-action-registry-vLYEFTtv.js +43 -0
- package/dist/index.js +129 -779
- package/dist/index.mjs +100 -750
- package/dist/kernel/config-manager.d.ts +4 -4
- package/dist/kernel/fs-utils.d.ts +16 -6
- package/dist/kernel/index.d.ts +1 -1
- package/dist/kernel/moleculer/device-cap-proxy.d.ts +2 -1
- package/dist/kernel/moleculer/readiness-context.d.ts +2 -1
- package/dist/kernel/transport/child-cap-protocol.d.ts +10 -0
- package/dist/{manifest-python-deps-B4BmMoGT.js → manifest-python-deps-BWURo7dc.js} +62 -88
- package/dist/{manifest-python-deps-CXbKrOdk.mjs → manifest-python-deps-BcrTzHH_.mjs} +55 -75
- package/dist/model-download-service-C7AjBsX9.mjs +668 -0
- package/dist/model-download-service-JtVQtbb6.js +752 -0
- package/dist/process/resource-monitor.d.ts +9 -0
- package/dist/{resource-monitor-ClDGFyf6.mjs → resource-monitor-BkP504Vq.mjs} +20 -1
- package/dist/{resource-monitor-IIEanuJt.js → resource-monitor-DNNomR-i.js} +21 -1
- package/package.json +6 -1
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
|
|
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,23 +19,25 @@ 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
|
|
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-BcrTzHH_.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 {
|
|
26
|
+
import { request } from "node:http";
|
|
25
27
|
import * as fs$17 from "node:fs";
|
|
26
|
-
import { accessSync, constants,
|
|
28
|
+
import { accessSync, constants, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
27
29
|
import * as path$40 from "node:path";
|
|
28
|
-
import
|
|
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
|
-
import { execFile,
|
|
33
|
+
import { execFile, spawn } from "node:child_process";
|
|
32
34
|
import * as util$8 from "node:util";
|
|
33
35
|
import { promisify } from "node:util";
|
|
34
36
|
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,459 +176,8 @@ 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
|
-
var execFileAsync$
|
|
180
|
+
var execFileAsync$3 = promisify(execFile);
|
|
839
181
|
var PythonEnvManager = class {
|
|
840
182
|
venvPath;
|
|
841
183
|
cachedProbe = null;
|
|
@@ -845,12 +187,12 @@ var PythonEnvManager = class {
|
|
|
845
187
|
async probe() {
|
|
846
188
|
if (this.cachedProbe) return this.cachedProbe;
|
|
847
189
|
for (const cmd of ["python3", "python"]) try {
|
|
848
|
-
const { stdout } = await execFileAsync$
|
|
190
|
+
const { stdout } = await execFileAsync$3(cmd, ["--version"]);
|
|
849
191
|
const version = stdout.trim().replace("Python ", "");
|
|
850
192
|
const major = parseInt(version.split(".")[0] ?? "0", 10);
|
|
851
193
|
const minor = parseInt(version.split(".")[1] ?? "0", 10);
|
|
852
194
|
if (major < 3 || major === 3 && minor < 10) continue;
|
|
853
|
-
const { stdout: pathOut } = await execFileAsync$
|
|
195
|
+
const { stdout: pathOut } = await execFileAsync$3(cmd, ["-c", "import sys; print(sys.executable)"]);
|
|
854
196
|
this.cachedProbe = {
|
|
855
197
|
available: true,
|
|
856
198
|
version,
|
|
@@ -866,13 +208,13 @@ var PythonEnvManager = class {
|
|
|
866
208
|
async ensure(options) {
|
|
867
209
|
const probe = await this.probe();
|
|
868
210
|
if (!probe.available || !probe.path) throw new Error("Python 3.10+ is required but not found on this system");
|
|
869
|
-
if (!fs$17.existsSync(path$40.join(this.venvPath, "bin", "python"))) await execFileAsync$
|
|
211
|
+
if (!fs$17.existsSync(path$40.join(this.venvPath, "bin", "python"))) await execFileAsync$3(probe.path, [
|
|
870
212
|
"-m",
|
|
871
213
|
"venv",
|
|
872
214
|
this.venvPath
|
|
873
215
|
]);
|
|
874
216
|
const venvPython = path$40.join(this.venvPath, "bin", "python");
|
|
875
|
-
if (options.packages.length > 0) await execFileAsync$
|
|
217
|
+
if (options.packages.length > 0) await execFileAsync$3(venvPython, [
|
|
876
218
|
"-m",
|
|
877
219
|
"pip",
|
|
878
220
|
"install",
|
|
@@ -2071,12 +1413,11 @@ var StorageManager = class {
|
|
|
2071
1413
|
return namespace ? this.createNamespacedLocation(location, namespace) : location;
|
|
2072
1414
|
}
|
|
2073
1415
|
createLegacyShim() {
|
|
2074
|
-
const self = this;
|
|
2075
1416
|
return {
|
|
2076
1417
|
async initialize() {},
|
|
2077
1418
|
async shutdown() {},
|
|
2078
|
-
getLocation(name) {
|
|
2079
|
-
return
|
|
1419
|
+
getLocation: (name) => {
|
|
1420
|
+
return this.getLocation(name);
|
|
2080
1421
|
}
|
|
2081
1422
|
};
|
|
2082
1423
|
}
|
|
@@ -2352,7 +1693,7 @@ function matchPath(pattern, path) {
|
|
|
2352
1693
|
}
|
|
2353
1694
|
//#endregion
|
|
2354
1695
|
//#region src/tls/cert-manager.ts
|
|
2355
|
-
var execFileAsync$
|
|
1696
|
+
var execFileAsync$2 = promisify(execFile);
|
|
2356
1697
|
/**
|
|
2357
1698
|
* Ensure a self-signed TLS certificate exists in the given directory.
|
|
2358
1699
|
* Generates one if missing. Returns paths to cert and key files.
|
|
@@ -2389,7 +1730,7 @@ async function ensureTlsCert(dataDir, options) {
|
|
|
2389
1730
|
for (const dns of sanDns) sanParts.push(`DNS:${dns}`);
|
|
2390
1731
|
for (const ip of sanIps) sanParts.push(`IP:${ip}`);
|
|
2391
1732
|
const sanString = sanParts.join(",");
|
|
2392
|
-
await execFileAsync$
|
|
1733
|
+
await execFileAsync$2("openssl", [
|
|
2393
1734
|
"req",
|
|
2394
1735
|
"-x509",
|
|
2395
1736
|
"-newkey",
|
|
@@ -3222,6 +2563,7 @@ var AddonEngineManager = class {
|
|
|
3222
2563
|
};
|
|
3223
2564
|
//#endregion
|
|
3224
2565
|
//#region src/kernel/fs-utils.ts
|
|
2566
|
+
var execFileAsync$1 = promisify(execFile);
|
|
3225
2567
|
/**
|
|
3226
2568
|
* Ensure a directory exists (recursive).
|
|
3227
2569
|
* Single source of truth — replaces scattered mkdirSync calls.
|
|
@@ -3230,18 +2572,20 @@ function ensureDir(dirPath) {
|
|
|
3230
2572
|
fs$17.mkdirSync(dirPath, { recursive: true });
|
|
3231
2573
|
}
|
|
3232
2574
|
/**
|
|
3233
|
-
* Copy a directory recursively.
|
|
3234
|
-
*
|
|
2575
|
+
* Copy a directory recursively — ASYNC so it never blocks the event loop.
|
|
2576
|
+
*
|
|
2577
|
+
* The hub installs addons into `/data/addons`, which on Unraid is a slow
|
|
2578
|
+
* shfs/FUSE mount. The former synchronous `fs.copyFileSync`-per-file loop
|
|
2579
|
+
* blocked the Node event loop for the whole copy of a large bundle (e.g.
|
|
2580
|
+
* `addon-pipeline`), which froze the hub's HTTP listener mid-`camstack deploy`
|
|
2581
|
+
* (accept backlog piling up, existing connections stuck in CLOSE_WAIT). Using
|
|
2582
|
+
* `fs.promises.cp` keeps the I/O off the event loop. (Node ≥18 stable.)
|
|
3235
2583
|
*/
|
|
3236
|
-
function copyDirRecursive(src, dest) {
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
const destPath = path$40.join(dest, entry.name);
|
|
3242
|
-
if (entry.isDirectory()) copyDirRecursive(srcPath, destPath);
|
|
3243
|
-
else fs$17.copyFileSync(srcPath, destPath);
|
|
3244
|
-
}
|
|
2584
|
+
async function copyDirRecursive(src, dest) {
|
|
2585
|
+
await fs$17.promises.cp(src, dest, {
|
|
2586
|
+
recursive: true,
|
|
2587
|
+
force: true
|
|
2588
|
+
});
|
|
3245
2589
|
}
|
|
3246
2590
|
/**
|
|
3247
2591
|
* Strip @camstack/* dependencies and devDependencies from a package.json object.
|
|
@@ -3271,7 +2615,7 @@ function stripCamstackDeps(pkg) {
|
|
|
3271
2615
|
* Copies directories (not individual files) from source to destination.
|
|
3272
2616
|
* Skips "dist" (already handled) and glob patterns.
|
|
3273
2617
|
*/
|
|
3274
|
-
function copyExtraFileDirs(pkgJson, sourceDir, destDir) {
|
|
2618
|
+
async function copyExtraFileDirs(pkgJson, sourceDir, destDir) {
|
|
3275
2619
|
const rawFiles = pkgJson.files;
|
|
3276
2620
|
if (!Array.isArray(rawFiles)) return;
|
|
3277
2621
|
for (const fileEntry of rawFiles) {
|
|
@@ -3280,11 +2624,11 @@ function copyExtraFileDirs(pkgJson, sourceDir, destDir) {
|
|
|
3280
2624
|
const srcPath = path$40.join(sourceDir, fileEntry);
|
|
3281
2625
|
if (!fs$17.existsSync(srcPath)) continue;
|
|
3282
2626
|
const destPath = path$40.join(destDir, fileEntry);
|
|
3283
|
-
const stat = fs$17.
|
|
3284
|
-
if (stat.isDirectory()) copyDirRecursive(srcPath, destPath);
|
|
2627
|
+
const stat = await fs$17.promises.stat(srcPath);
|
|
2628
|
+
if (stat.isDirectory()) await copyDirRecursive(srcPath, destPath);
|
|
3285
2629
|
else if (stat.isFile()) {
|
|
3286
2630
|
ensureDir(path$40.dirname(destPath));
|
|
3287
|
-
fs$17.
|
|
2631
|
+
await fs$17.promises.copyFile(srcPath, destPath);
|
|
3288
2632
|
}
|
|
3289
2633
|
}
|
|
3290
2634
|
}
|
|
@@ -3329,12 +2673,16 @@ function ensureLibraryBuilt(packageName, packagesDir) {
|
|
|
3329
2673
|
/**
|
|
3330
2674
|
* Install a single npm package into a target directory (package.json + dist/).
|
|
3331
2675
|
* No validation on camstack.addons -- works for any @camstack/* package.
|
|
3332
|
-
*
|
|
2676
|
+
*
|
|
2677
|
+
* ASYNC: uses `execFile`/`fs.promises` throughout so a package install on a
|
|
2678
|
+
* live node never blocks the event loop (the `npm pack` + tar extract + copy to
|
|
2679
|
+
* the slow shfs/FUSE `/data` would otherwise freeze the HTTP listener). See
|
|
2680
|
+
* `copyDirRecursive` for the wedge this prevents.
|
|
3333
2681
|
*/
|
|
3334
|
-
function
|
|
3335
|
-
const tmpDir = fs$17.
|
|
2682
|
+
async function installPackageFromNpm(packageName, targetDir) {
|
|
2683
|
+
const tmpDir = await fs$17.promises.mkdtemp(path$40.join(os$17.tmpdir(), "camstack-install-"));
|
|
3336
2684
|
try {
|
|
3337
|
-
const
|
|
2685
|
+
const { stdout } = await execFileAsync$1("npm", [
|
|
3338
2686
|
"pack",
|
|
3339
2687
|
packageName,
|
|
3340
2688
|
"--pack-destination",
|
|
@@ -3342,12 +2690,13 @@ function installPackageFromNpmSync(packageName, targetDir) {
|
|
|
3342
2690
|
], {
|
|
3343
2691
|
timeout: 12e4,
|
|
3344
2692
|
encoding: "utf-8"
|
|
3345
|
-
})
|
|
2693
|
+
});
|
|
2694
|
+
const tgzFilename = stdout.trim().split("\n").pop()?.trim();
|
|
3346
2695
|
if (!tgzFilename) throw new Error("npm pack produced no output");
|
|
3347
2696
|
const tgzPath = path$40.join(tmpDir, tgzFilename);
|
|
3348
2697
|
const extractDir = path$40.join(tmpDir, "extracted");
|
|
3349
2698
|
ensureDir(extractDir);
|
|
3350
|
-
|
|
2699
|
+
await execFileAsync$1("tar", [
|
|
3351
2700
|
"-xzf",
|
|
3352
2701
|
tgzPath,
|
|
3353
2702
|
"-C",
|
|
@@ -3355,20 +2704,20 @@ function installPackageFromNpmSync(packageName, targetDir) {
|
|
|
3355
2704
|
], { timeout: 3e4 });
|
|
3356
2705
|
const packageSubDir = path$40.join(extractDir, "package");
|
|
3357
2706
|
const srcPkgJsonDir = fs$17.existsSync(path$40.join(packageSubDir, "package.json")) ? packageSubDir : extractDir;
|
|
3358
|
-
fs$17.
|
|
2707
|
+
await fs$17.promises.rm(targetDir, {
|
|
3359
2708
|
recursive: true,
|
|
3360
2709
|
force: true
|
|
3361
2710
|
});
|
|
3362
2711
|
ensureDir(targetDir);
|
|
3363
|
-
fs$17.
|
|
2712
|
+
await fs$17.promises.copyFile(path$40.join(srcPkgJsonDir, "package.json"), path$40.join(targetDir, "package.json"));
|
|
3364
2713
|
const distSrc = path$40.join(srcPkgJsonDir, "dist");
|
|
3365
|
-
if (fs$17.existsSync(distSrc)) copyDirRecursive(distSrc, path$40.join(targetDir, "dist"));
|
|
2714
|
+
if (fs$17.existsSync(distSrc)) await copyDirRecursive(distSrc, path$40.join(targetDir, "dist"));
|
|
3366
2715
|
try {
|
|
3367
|
-
const npmPkg = asJsonObject(parseJsonUnknown(fs$17.
|
|
3368
|
-
if (npmPkg) copyExtraFileDirs(npmPkg, srcPkgJsonDir, targetDir);
|
|
2716
|
+
const npmPkg = asJsonObject(parseJsonUnknown(await fs$17.promises.readFile(path$40.join(srcPkgJsonDir, "package.json"), "utf-8")));
|
|
2717
|
+
if (npmPkg) await copyExtraFileDirs(npmPkg, srcPkgJsonDir, targetDir);
|
|
3369
2718
|
} catch {}
|
|
3370
2719
|
} finally {
|
|
3371
|
-
fs$17.
|
|
2720
|
+
await fs$17.promises.rm(tmpDir, {
|
|
3372
2721
|
recursive: true,
|
|
3373
2722
|
force: true
|
|
3374
2723
|
});
|
|
@@ -3818,15 +3167,15 @@ var AddonInstaller = class AddonInstaller {
|
|
|
3818
3167
|
await this.ensureBuilt(packageName, sourceDir, pkgData);
|
|
3819
3168
|
const distDir = path$40.join(sourceDir, "dist");
|
|
3820
3169
|
if (!fs$17.existsSync(distDir)) throw new Error(`${packageName} has no dist/ after build`);
|
|
3821
|
-
fs$17.
|
|
3170
|
+
await fs$17.promises.rm(targetDir, {
|
|
3822
3171
|
recursive: true,
|
|
3823
3172
|
force: true
|
|
3824
3173
|
});
|
|
3825
3174
|
ensureDir(targetDir);
|
|
3826
|
-
fs$17.
|
|
3827
|
-
copyDirRecursive(distDir, path$40.join(targetDir, "dist"));
|
|
3828
|
-
copyExtraFileDirs(pkgData, sourceDir, targetDir);
|
|
3829
|
-
fs$17.
|
|
3175
|
+
await fs$17.promises.writeFile(path$40.join(targetDir, "package.json"), JSON.stringify(stripCamstackDeps(pkgData), null, 2));
|
|
3176
|
+
await copyDirRecursive(distDir, path$40.join(targetDir, "dist"));
|
|
3177
|
+
await copyExtraFileDirs(pkgData, sourceDir, targetDir);
|
|
3178
|
+
await fs$17.promises.writeFile(path$40.join(targetDir, ".install-source"), "local");
|
|
3830
3179
|
const localPkgVersion = asString(pkgData.version, "0.0.0");
|
|
3831
3180
|
this.manifest.upsert(packageName, {
|
|
3832
3181
|
version: localPkgVersion,
|
|
@@ -3946,17 +3295,17 @@ var AddonInstaller = class AddonInstaller {
|
|
|
3946
3295
|
if (!pkgView) throw new Error(`Invalid package.json at ${pkgJsonPath}`);
|
|
3947
3296
|
if (!pkgView.camstackAddons) throw new Error(`Package ${pkgView.name} has no camstack.addons manifest`);
|
|
3948
3297
|
const targetDir = path$40.join(this.addonsDir, pkgView.name);
|
|
3949
|
-
fs$17.
|
|
3298
|
+
await fs$17.promises.rm(targetDir, {
|
|
3950
3299
|
recursive: true,
|
|
3951
3300
|
force: true
|
|
3952
3301
|
});
|
|
3953
3302
|
ensureDir(targetDir);
|
|
3954
3303
|
const sourceDir = path$40.dirname(pkgJsonPath);
|
|
3955
3304
|
const strippedManifest = stripCamstackDeps(pkgView.raw);
|
|
3956
|
-
fs$17.
|
|
3305
|
+
await fs$17.promises.writeFile(path$40.join(targetDir, "package.json"), JSON.stringify(strippedManifest, null, 2));
|
|
3957
3306
|
const sourceDist = path$40.join(sourceDir, "dist");
|
|
3958
|
-
if (fs$17.existsSync(sourceDist)) copyDirRecursive(sourceDist, path$40.join(targetDir, "dist"));
|
|
3959
|
-
copyExtraFileDirs(pkgView.raw, sourceDir, targetDir);
|
|
3307
|
+
if (fs$17.existsSync(sourceDist)) await copyDirRecursive(sourceDist, path$40.join(targetDir, "dist"));
|
|
3308
|
+
await copyExtraFileDirs(pkgView.raw, sourceDir, targetDir);
|
|
3960
3309
|
const strippedRuntimeDeps = strippedManifest["dependencies"];
|
|
3961
3310
|
if (strippedRuntimeDeps != null && typeof strippedRuntimeDeps === "object" && Object.keys(strippedRuntimeDeps).length > 0) {
|
|
3962
3311
|
this.logger.info(`${pkgView.name} — installing runtime dependencies`, { meta: { targetDir } });
|
|
@@ -3980,7 +3329,7 @@ var AddonInstaller = class AddonInstaller {
|
|
|
3980
3329
|
try {
|
|
3981
3330
|
await installManifestNativeDeps(targetDir, pkgView.raw, this.logger, this.registry);
|
|
3982
3331
|
} catch (nativeErr) {
|
|
3983
|
-
fs$17.
|
|
3332
|
+
await fs$17.promises.rm(targetDir, {
|
|
3984
3333
|
recursive: true,
|
|
3985
3334
|
force: true
|
|
3986
3335
|
});
|
|
@@ -3995,7 +3344,7 @@ var AddonInstaller = class AddonInstaller {
|
|
|
3995
3344
|
version: pkgView.version
|
|
3996
3345
|
};
|
|
3997
3346
|
} finally {
|
|
3998
|
-
fs$17.
|
|
3347
|
+
await fs$17.promises.rm(tmpDir, {
|
|
3999
3348
|
recursive: true,
|
|
4000
3349
|
force: true
|
|
4001
3350
|
});
|
|
@@ -4763,7 +4112,7 @@ var CapabilityRegistry = class CapabilityRegistry {
|
|
|
4763
4112
|
const bare = this.bareAddonId(addonId);
|
|
4764
4113
|
const nodeMap = this.singletonNodeOverrides.get(capabilityName);
|
|
4765
4114
|
if (nodeMap) {
|
|
4766
|
-
for (const [nodeId, ov] of
|
|
4115
|
+
for (const [nodeId, ov] of Array.from(nodeMap)) if (ov === bare) nodeMap.delete(nodeId);
|
|
4767
4116
|
if (nodeMap.size === 0) this.singletonNodeOverrides.delete(capabilityName);
|
|
4768
4117
|
}
|
|
4769
4118
|
this.logger.info("Provider unregistered from capability", {
|
|
@@ -8025,7 +7374,7 @@ var ConfigManager = class ConfigManager {
|
|
|
8025
7374
|
constructor(configPath) {
|
|
8026
7375
|
this.configPath = configPath;
|
|
8027
7376
|
const rawYaml = this.loadYaml();
|
|
8028
|
-
const merged = this.applyEnvOverrides(asJsonObject(rawYaml) ?? {});
|
|
7377
|
+
const merged = this.applyEnvOverrides(asJsonObject$1(rawYaml) ?? {});
|
|
8029
7378
|
this.bootstrapConfig = bootstrapSchema.parse(merged);
|
|
8030
7379
|
this.warnDefaultCredentials();
|
|
8031
7380
|
const dataPath = this.bootstrapConfig.server.dataPath ?? "camstack-data";
|
|
@@ -8040,20 +7389,20 @@ var ConfigManager = class ConfigManager {
|
|
|
8040
7389
|
setSettingsStore(store) {
|
|
8041
7390
|
this.settingsStore = store;
|
|
8042
7391
|
}
|
|
8043
|
-
get(
|
|
8044
|
-
return this.resolveConfigValue(
|
|
7392
|
+
get(configPath) {
|
|
7393
|
+
return this.resolveConfigValue(configPath);
|
|
8045
7394
|
}
|
|
8046
|
-
resolveConfigValue(
|
|
8047
|
-
const bootstrapValue = this.getFromBootstrap(
|
|
7395
|
+
resolveConfigValue(configPath) {
|
|
7396
|
+
const bootstrapValue = this.getFromBootstrap(configPath);
|
|
8048
7397
|
if (bootstrapValue !== void 0) return bootstrapValue;
|
|
8049
7398
|
if (this.settingsStore !== null) {
|
|
8050
|
-
const storeValue = this.settingsStore.getSystem(
|
|
7399
|
+
const storeValue = this.settingsStore.getSystem(configPath);
|
|
8051
7400
|
if (storeValue !== void 0) return storeValue;
|
|
8052
|
-
const nested = this.getNestedFromSystemSettings(
|
|
7401
|
+
const nested = this.getNestedFromSystemSettings(configPath);
|
|
8053
7402
|
if (nested !== null) return nested;
|
|
8054
7403
|
}
|
|
8055
|
-
if (
|
|
8056
|
-
return this.getFromRuntimeDefaults(
|
|
7404
|
+
if (configPath in RUNTIME_DEFAULTS) return RUNTIME_DEFAULTS[configPath];
|
|
7405
|
+
return this.getFromRuntimeDefaults(configPath) ?? void 0;
|
|
8057
7406
|
}
|
|
8058
7407
|
/**
|
|
8059
7408
|
* Write a value to the settings-store.
|
|
@@ -8080,9 +7429,9 @@ var ConfigManager = class ConfigManager {
|
|
|
8080
7429
|
*/
|
|
8081
7430
|
getSection(section) {
|
|
8082
7431
|
const merged = {};
|
|
8083
|
-
const defaults = asJsonObject(this.getFromRuntimeDefaults(section));
|
|
7432
|
+
const defaults = asJsonObject$1(this.getFromRuntimeDefaults(section));
|
|
8084
7433
|
if (defaults) Object.assign(merged, defaults);
|
|
8085
|
-
const bootstrapSection = asJsonObject({ ...this.bootstrapConfig }[section]);
|
|
7434
|
+
const bootstrapSection = asJsonObject$1({ ...this.bootstrapConfig }[section]);
|
|
8086
7435
|
if (bootstrapSection) Object.assign(merged, bootstrapSection);
|
|
8087
7436
|
if (this.settingsStore !== null) {
|
|
8088
7437
|
const nested = this.getNestedFromSystemSettings(section);
|
|
@@ -8101,7 +7450,7 @@ var ConfigManager = class ConfigManager {
|
|
|
8101
7450
|
/** Read all config for an addon from addon_settings. */
|
|
8102
7451
|
getAddonConfig(addonId) {
|
|
8103
7452
|
if (this.settingsStore !== null) return this.settingsStore.getAllAddon(addonId);
|
|
8104
|
-
return asJsonObject(this.getFromBootstrap(`addons.${addonId}`)) ?? {};
|
|
7453
|
+
return asJsonObject$1(this.getFromBootstrap(`addons.${addonId}`)) ?? {};
|
|
8105
7454
|
}
|
|
8106
7455
|
/** Write (bulk-replace) config for an addon to addon_settings. */
|
|
8107
7456
|
setAddonConfig(addonId, config) {
|
|
@@ -8269,8 +7618,8 @@ var ConfigManager = class ConfigManager {
|
|
|
8269
7618
|
};
|
|
8270
7619
|
this.saveRuntimeState();
|
|
8271
7620
|
}
|
|
8272
|
-
getBootstrap(
|
|
8273
|
-
return this.getFromBootstrap(
|
|
7621
|
+
getBootstrap(configPath) {
|
|
7622
|
+
return this.getFromBootstrap(configPath);
|
|
8274
7623
|
}
|
|
8275
7624
|
/** Features accessor -- reads from settings-store when available, falls back to RUNTIME_DEFAULTS */
|
|
8276
7625
|
get features() {
|
|
@@ -8339,8 +7688,8 @@ var ConfigManager = class ConfigManager {
|
|
|
8339
7688
|
update(section, data) {
|
|
8340
7689
|
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
7690
|
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]) ?? {};
|
|
7691
|
+
if (fs$17.existsSync(this.configPath)) raw = asJsonObject$1(load$1(fs$17.readFileSync(this.configPath, "utf-8"))) ?? {};
|
|
7692
|
+
const existing = asJsonObject$1(raw[section]) ?? {};
|
|
8344
7693
|
raw[section] = {
|
|
8345
7694
|
...existing,
|
|
8346
7695
|
...data
|
|
@@ -8360,8 +7709,8 @@ var ConfigManager = class ConfigManager {
|
|
|
8360
7709
|
* Deep-set a value in a nested plain object using a dot-notation path.
|
|
8361
7710
|
* Returns a new object (immutable).
|
|
8362
7711
|
*/
|
|
8363
|
-
setNested(obj,
|
|
8364
|
-
const [head, ...rest] =
|
|
7712
|
+
setNested(obj, configPath, value) {
|
|
7713
|
+
const [head, ...rest] = configPath.split(".");
|
|
8365
7714
|
if (!head) return obj;
|
|
8366
7715
|
if (rest.length === 0) return {
|
|
8367
7716
|
...obj,
|
|
@@ -8401,8 +7750,8 @@ var ConfigManager = class ConfigManager {
|
|
|
8401
7750
|
warnDefaultCredentials() {
|
|
8402
7751
|
if (this.bootstrapConfig.auth.adminPassword === "changeme") console.warn("[ConfigManager] Warning: Using default admin password \"changeme\". Set auth.adminPassword in your config.yaml or the CAMSTACK_ADMIN_PASS env var.");
|
|
8403
7752
|
}
|
|
8404
|
-
getFromBootstrap(
|
|
8405
|
-
const keys =
|
|
7753
|
+
getFromBootstrap(configPath) {
|
|
7754
|
+
const keys = configPath.split(".");
|
|
8406
7755
|
let current = this.bootstrapConfig;
|
|
8407
7756
|
for (const key of keys) {
|
|
8408
7757
|
if (!isRecord(current)) return void 0;
|
|
@@ -8410,8 +7759,8 @@ var ConfigManager = class ConfigManager {
|
|
|
8410
7759
|
}
|
|
8411
7760
|
return current;
|
|
8412
7761
|
}
|
|
8413
|
-
getFromRuntimeDefaults(
|
|
8414
|
-
const prefix =
|
|
7762
|
+
getFromRuntimeDefaults(configPath) {
|
|
7763
|
+
const prefix = configPath + ".";
|
|
8415
7764
|
const result = {};
|
|
8416
7765
|
let found = false;
|
|
8417
7766
|
for (const [key, value] of Object.entries(RUNTIME_DEFAULTS)) if (key.startsWith(prefix)) {
|
|
@@ -8426,10 +7775,10 @@ var ConfigManager = class ConfigManager {
|
|
|
8426
7775
|
* e.g. path='features' matches keys 'features.streaming', 'features.notifications', etc.
|
|
8427
7776
|
* Returns an object keyed by the sub-key, or undefined if nothing is found.
|
|
8428
7777
|
*/
|
|
8429
|
-
getNestedFromSystemSettings(
|
|
7778
|
+
getNestedFromSystemSettings(configPath) {
|
|
8430
7779
|
if (this.settingsStore === null) return null;
|
|
8431
7780
|
const all = this.settingsStore.getAllSystem();
|
|
8432
|
-
const prefix =
|
|
7781
|
+
const prefix = configPath + ".";
|
|
8433
7782
|
const result = {};
|
|
8434
7783
|
let found = false;
|
|
8435
7784
|
for (const [key, value] of Object.entries(all)) if (key.startsWith(prefix)) {
|
|
@@ -8441,13 +7790,13 @@ var ConfigManager = class ConfigManager {
|
|
|
8441
7790
|
loadRuntimeState() {
|
|
8442
7791
|
if (!fs$17.existsSync(this.runtimeStatePath)) return EMPTY_RUNTIME_STATE;
|
|
8443
7792
|
try {
|
|
8444
|
-
const parsed = asJsonObject(parseJsonUnknown(fs$17.readFileSync(this.runtimeStatePath, "utf-8")));
|
|
7793
|
+
const parsed = asJsonObject$1(parseJsonUnknown$1(fs$17.readFileSync(this.runtimeStatePath, "utf-8")));
|
|
8445
7794
|
if (parsed === null) return EMPTY_RUNTIME_STATE;
|
|
8446
|
-
const systemActivation = asJsonObject(parsed.systemActivation) ?? {};
|
|
8447
|
-
const deviceActivationRaw = asJsonObject(parsed.deviceActivation) ?? {};
|
|
7795
|
+
const systemActivation = asJsonObject$1(parsed.systemActivation) ?? {};
|
|
7796
|
+
const deviceActivationRaw = asJsonObject$1(parsed.deviceActivation) ?? {};
|
|
8448
7797
|
const deviceActivation = {};
|
|
8449
7798
|
for (const [deviceId, entry] of Object.entries(deviceActivationRaw)) {
|
|
8450
|
-
const nested = asJsonObject(entry);
|
|
7799
|
+
const nested = asJsonObject$1(entry);
|
|
8451
7800
|
if (nested === null) continue;
|
|
8452
7801
|
const bools = {};
|
|
8453
7802
|
for (const [k, v] of Object.entries(nested)) if (typeof v === "boolean") bools[k] = v;
|
|
@@ -91590,7 +90939,7 @@ function createProcessService(parentNodeId, dataDir, deps, parentTcpPort, parent
|
|
|
91590
90939
|
respawned.restartCount = prevRestartCount + 1;
|
|
91591
90940
|
});
|
|
91592
90941
|
restarted.push(name);
|
|
91593
|
-
} catch
|
|
90942
|
+
} catch {
|
|
91594
90943
|
failed.push(name);
|
|
91595
90944
|
}
|
|
91596
90945
|
return {
|
|
@@ -91985,8 +91334,8 @@ var LifecycleJobEngine = class {
|
|
|
91985
91334
|
} finally {
|
|
91986
91335
|
clearTimeout(timer);
|
|
91987
91336
|
}
|
|
91988
|
-
if (packages.length === 0) throw new Error("stageFramework returned no packages");
|
|
91989
91337
|
const [firstPackage] = packages;
|
|
91338
|
+
if (!firstPackage) throw new Error("stageFramework returned no packages");
|
|
91990
91339
|
this.advance(job.jobId, task, "staged", { stagedPath: firstPackage.stagedPath });
|
|
91991
91340
|
requestFrameworkSwap({
|
|
91992
91341
|
jobId: job.jobId,
|
|
@@ -92009,14 +91358,15 @@ var LifecycleJobEngine = class {
|
|
|
92009
91358
|
*/
|
|
92010
91359
|
async fetchAddonsBounded(job, addonTasks) {
|
|
92011
91360
|
const limit = Math.max(1, this.deps.fetchConcurrency ?? DEFAULT_FETCH_CONCURRENCY);
|
|
92012
|
-
const outcomes =
|
|
91361
|
+
const outcomes = Array.from({ length: addonTasks.length });
|
|
92013
91362
|
let nextIndex = 0;
|
|
92014
91363
|
const worker = async () => {
|
|
92015
91364
|
for (;;) {
|
|
92016
91365
|
const index = nextIndex;
|
|
92017
91366
|
nextIndex += 1;
|
|
92018
|
-
|
|
92019
|
-
|
|
91367
|
+
const addonTask = addonTasks[index];
|
|
91368
|
+
if (addonTask === void 0) return;
|
|
91369
|
+
outcomes[index] = await this.fetchAddonTask(job, addonTask);
|
|
92020
91370
|
}
|
|
92021
91371
|
};
|
|
92022
91372
|
const workerCount = Math.min(limit, addonTasks.length);
|
|
@@ -92142,4 +91492,4 @@ async function stageFrameworkLockstep(input) {
|
|
|
92142
91492
|
return results;
|
|
92143
91493
|
}
|
|
92144
91494
|
//#endregion
|
|
92145
|
-
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,
|
|
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 };
|