@camstack/kernel 0.1.7 → 0.1.10

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.js CHANGED
@@ -5,9 +5,6 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __commonJS = (cb, mod) => function __require() {
9
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
- };
11
8
  var __export = (target, all) => {
12
9
  for (var name in all)
13
10
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -30,2125 +27,1845 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
27
  ));
31
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
29
 
33
- // src/addon-loader.js
34
- var require_addon_loader = __commonJS({
35
- "src/addon-loader.js"(exports2) {
36
- "use strict";
37
- var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
38
- if (k2 === void 0) k2 = k;
39
- var desc = Object.getOwnPropertyDescriptor(m, k);
40
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
41
- desc = { enumerable: true, get: function() {
42
- return m[k];
43
- } };
44
- }
45
- Object.defineProperty(o, k2, desc);
46
- }) : (function(o, m, k, k2) {
47
- if (k2 === void 0) k2 = k;
48
- o[k2] = m[k];
49
- }));
50
- var __setModuleDefault = exports2 && exports2.__setModuleDefault || (Object.create ? (function(o, v) {
51
- Object.defineProperty(o, "default", { enumerable: true, value: v });
52
- }) : function(o, v) {
53
- o["default"] = v;
30
+ // src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ AddonEngineManager: () => AddonEngineManager,
34
+ AddonInstaller: () => AddonInstaller,
35
+ AddonLoader: () => AddonLoader,
36
+ AddonWorkerHost: () => AddonWorkerHost,
37
+ CapabilityRegistry: () => CapabilityRegistry,
38
+ ConfigManager: () => ConfigManager,
39
+ DEFAULT_DATA_PATH: () => DEFAULT_DATA_PATH,
40
+ INFRA_CAPABILITIES: () => INFRA_CAPABILITIES,
41
+ RUNTIME_DEFAULTS: () => RUNTIME_DEFAULTS,
42
+ WorkerProcessManager: () => WorkerProcessManager,
43
+ bootstrapSchema: () => bootstrapSchema,
44
+ copyDirRecursive: () => copyDirRecursive,
45
+ copyExtraFileDirs: () => copyExtraFileDirs,
46
+ detectWorkspacePackagesDir: () => detectWorkspacePackagesDir,
47
+ ensureDir: () => ensureDir,
48
+ ensureLibraryBuilt: () => ensureLibraryBuilt,
49
+ installPackageFromNpmSync: () => installPackageFromNpmSync,
50
+ isInfraCapability: () => isInfraCapability,
51
+ isSourceNewer: () => isSourceNewer,
52
+ stripCamstackDeps: () => stripCamstackDeps,
53
+ symlinkAddonsToNodeModules: () => symlinkAddonsToNodeModules
54
+ });
55
+ module.exports = __toCommonJS(src_exports);
56
+
57
+ // src/addon-loader.ts
58
+ var fs = __toESM(require("fs"));
59
+ var path = __toESM(require("path"));
60
+ function resolveAddonClass(mod) {
61
+ let candidate = mod["default"] ?? mod[Object.keys(mod)[0]];
62
+ if (candidate && typeof candidate === "object" && "default" in candidate) {
63
+ candidate = candidate["default"];
64
+ }
65
+ if (candidate && typeof candidate === "object") {
66
+ const obj = candidate;
67
+ const fn = Object.values(obj).find((v) => typeof v === "function");
68
+ if (fn) candidate = fn;
69
+ }
70
+ if (typeof candidate !== "function") {
71
+ candidate = Object.values(mod).find((v) => typeof v === "function");
72
+ }
73
+ return typeof candidate === "function" ? candidate : void 0;
74
+ }
75
+ var AddonLoader = class {
76
+ addons = /* @__PURE__ */ new Map();
77
+ /** Scan addons directory and load all addon packages.
78
+ * Supports scoped layout: addons/@scope/package-name/ */
79
+ async loadFromDirectory(addonsDir) {
80
+ if (!fs.existsSync(addonsDir)) return;
81
+ const entries = fs.readdirSync(addonsDir, { withFileTypes: true });
82
+ for (const entry of entries) {
83
+ if (!entry.isDirectory()) continue;
84
+ if (entry.name.startsWith("@")) {
85
+ const scopeDir = path.join(addonsDir, entry.name);
86
+ const scopeEntries = fs.readdirSync(scopeDir, { withFileTypes: true });
87
+ for (const scopeEntry of scopeEntries) {
88
+ if (!scopeEntry.isDirectory()) continue;
89
+ await this.tryLoadAddon(path.join(scopeDir, scopeEntry.name));
90
+ }
91
+ continue;
92
+ }
93
+ await this.tryLoadAddon(path.join(addonsDir, entry.name));
94
+ }
95
+ }
96
+ async tryLoadAddon(addonDir) {
97
+ const pkgJsonPath = path.join(addonDir, "package.json");
98
+ if (!fs.existsSync(pkgJsonPath)) return;
99
+ try {
100
+ await this.loadFromAddonDir(addonDir);
101
+ } catch (err) {
102
+ console.warn(`Failed to load addon from ${addonDir}: ${err}`);
103
+ }
104
+ }
105
+ /** Load addon from a specific directory (package.json + dist/) */
106
+ async loadFromAddonDir(addonDir) {
107
+ const pkgJsonPath = path.join(addonDir, "package.json");
108
+ const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
109
+ const packageName = pkgJson["name"];
110
+ const packageVersion = pkgJson["version"] ?? "0.0.0";
111
+ const manifest = pkgJson["camstack"];
112
+ if (!manifest?.addons?.length) return;
113
+ for (const declaration of manifest.addons) {
114
+ const entryFile = declaration.entry.replace(/^\.\//, "").replace(/^src\//, "dist/").replace(/\.ts$/, ".js");
115
+ let entryPath = path.resolve(addonDir, entryFile);
116
+ if (!fs.existsSync(entryPath)) {
117
+ const base = entryPath.replace(/\.(js|cjs|mjs)$/, "");
118
+ const alternatives = [
119
+ `${base}.cjs`,
120
+ `${base}.mjs`,
121
+ path.resolve(addonDir, "dist", "index.js"),
122
+ path.resolve(addonDir, "dist", "index.cjs"),
123
+ path.resolve(addonDir, "dist", "index.mjs"),
124
+ path.resolve(addonDir, declaration.entry)
125
+ ];
126
+ entryPath = alternatives.find((p) => fs.existsSync(p)) ?? entryPath;
127
+ }
128
+ if (!fs.existsSync(entryPath)) {
129
+ throw new Error(`Entry not found: ${entryPath}`);
130
+ }
131
+ const mod = await import(entryPath);
132
+ const AddonClass = resolveAddonClass(mod);
133
+ if (!AddonClass) {
134
+ throw new Error(`No addon class in ${entryPath}`);
135
+ }
136
+ this.addons.set(declaration.id, {
137
+ declaration,
138
+ packageName,
139
+ packageVersion,
140
+ packageDisplayName: manifest.displayName,
141
+ addonClass: AddonClass
142
+ });
143
+ }
144
+ }
145
+ /** Load addon from a direct path (for development/testing) */
146
+ async loadFromPath(addonId, modulePath, packageName, declaration, packageVersion = "0.0.0") {
147
+ const mod = await import(modulePath);
148
+ const AddonClass = resolveAddonClass(mod);
149
+ if (!AddonClass) {
150
+ throw new Error(`Module ${modulePath} has no default export`);
151
+ }
152
+ this.addons.set(addonId, {
153
+ declaration: {
154
+ id: addonId,
155
+ entry: modulePath,
156
+ slot: "detector",
157
+ ...declaration
158
+ },
159
+ packageName,
160
+ packageVersion,
161
+ addonClass: AddonClass
54
162
  });
55
- var __importStar = exports2 && exports2.__importStar || /* @__PURE__ */ (function() {
56
- var ownKeys = function(o) {
57
- ownKeys = Object.getOwnPropertyNames || function(o2) {
58
- var ar = [];
59
- for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
60
- return ar;
61
- };
62
- return ownKeys(o);
63
- };
64
- return function(mod) {
65
- if (mod && mod.__esModule) return mod;
66
- var result = {};
67
- if (mod != null) {
68
- for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
69
- }
70
- __setModuleDefault(result, mod);
71
- return result;
72
- };
73
- })();
74
- Object.defineProperty(exports2, "__esModule", { value: true });
75
- exports2.AddonLoader = void 0;
76
- var fs = __importStar(require("fs"));
77
- var path = __importStar(require("path"));
78
- function resolveAddonClass(mod) {
79
- let candidate = mod["default"] ?? mod[Object.keys(mod)[0]];
80
- if (candidate && typeof candidate === "object" && "default" in candidate) {
81
- candidate = candidate["default"];
82
- }
83
- return typeof candidate === "function" ? candidate : void 0;
84
- }
85
- var AddonLoader2 = class {
86
- addons = /* @__PURE__ */ new Map();
87
- /** Scan addons directory and load all addon packages.
88
- * Supports scoped layout: addons/@scope/package-name/ */
89
- async loadFromDirectory(addonsDir) {
90
- if (!fs.existsSync(addonsDir))
91
- return;
92
- const entries = fs.readdirSync(addonsDir, { withFileTypes: true });
93
- for (const entry of entries) {
94
- if (!entry.isDirectory())
95
- continue;
96
- if (entry.name.startsWith("@")) {
97
- const scopeDir = path.join(addonsDir, entry.name);
98
- const scopeEntries = fs.readdirSync(scopeDir, { withFileTypes: true });
99
- for (const scopeEntry of scopeEntries) {
100
- if (!scopeEntry.isDirectory())
101
- continue;
102
- await this.tryLoadAddon(path.join(scopeDir, scopeEntry.name));
103
- }
104
- continue;
105
- }
106
- await this.tryLoadAddon(path.join(addonsDir, entry.name));
107
- }
108
- }
109
- async tryLoadAddon(addonDir) {
110
- const pkgJsonPath = path.join(addonDir, "package.json");
111
- if (!fs.existsSync(pkgJsonPath))
112
- return;
113
- try {
114
- await this.loadFromAddonDir(addonDir);
115
- } catch (err) {
116
- console.warn(`Failed to load addon from ${addonDir}: ${err}`);
117
- }
118
- }
119
- /** Load addon from a specific directory (package.json + dist/) */
120
- async loadFromAddonDir(addonDir) {
121
- const pkgJsonPath = path.join(addonDir, "package.json");
122
- const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
123
- const packageName = pkgJson["name"];
124
- const packageVersion = pkgJson["version"] ?? "0.0.0";
125
- const manifest = pkgJson["camstack"];
126
- if (!manifest?.addons?.length)
127
- return;
128
- for (const declaration of manifest.addons) {
129
- const entryFile = declaration.entry.replace(/^\.\//, "").replace(/^src\//, "dist/").replace(/\.ts$/, ".js");
130
- let entryPath = path.resolve(addonDir, entryFile);
131
- if (!fs.existsSync(entryPath)) {
132
- const base = entryPath.replace(/\.(js|cjs|mjs)$/, "");
133
- const alternatives = [
134
- `${base}.cjs`,
135
- `${base}.mjs`,
136
- path.resolve(addonDir, "dist", "index.js"),
137
- path.resolve(addonDir, "dist", "index.cjs"),
138
- path.resolve(addonDir, "dist", "index.mjs"),
139
- path.resolve(addonDir, declaration.entry)
140
- ];
141
- entryPath = alternatives.find((p) => fs.existsSync(p)) ?? entryPath;
142
- }
143
- if (!fs.existsSync(entryPath)) {
144
- throw new Error(`Entry not found: ${entryPath}`);
145
- }
146
- const mod = await Promise.resolve(`${entryPath}`).then((s) => __importStar(require(s)));
147
- const AddonClass = resolveAddonClass(mod);
148
- if (!AddonClass) {
149
- throw new Error(`No addon class in ${entryPath}`);
150
- }
151
- this.addons.set(declaration.id, {
152
- declaration,
153
- packageName,
154
- packageVersion,
155
- packageDisplayName: manifest.displayName,
156
- addonClass: AddonClass
157
- });
158
- }
159
- }
160
- /** Load addon from a direct path (for development/testing) */
161
- async loadFromPath(addonId, modulePath, packageName, declaration, packageVersion = "0.0.0") {
162
- const mod = await Promise.resolve(`${modulePath}`).then((s) => __importStar(require(s)));
163
- const AddonClass = resolveAddonClass(mod);
164
- if (!AddonClass) {
165
- throw new Error(`Module ${modulePath} has no default export`);
166
- }
167
- this.addons.set(addonId, {
168
- declaration: {
169
- id: addonId,
170
- entry: modulePath,
171
- slot: "detector",
172
- ...declaration
173
- },
174
- packageName,
175
- packageVersion,
176
- addonClass: AddonClass
177
- });
178
- }
179
- /** Get a registered addon by ID */
180
- getAddon(addonId) {
181
- return this.addons.get(addonId);
182
- }
183
- /** List all registered addons */
184
- listAddons() {
185
- return [...this.addons.values()];
186
- }
187
- /** Check if an addon is registered */
188
- hasAddon(addonId) {
189
- return this.addons.has(addonId);
190
- }
191
- /** Create a new instance of an addon (not yet initialized) */
192
- createInstance(addonId) {
193
- const registered = this.addons.get(addonId);
194
- if (!registered) {
195
- throw new Error(`Addon "${addonId}" is not registered`);
196
- }
197
- return new registered.addonClass();
198
- }
199
- };
200
- exports2.AddonLoader = AddonLoader2;
201
163
  }
202
- });
164
+ /** Get a registered addon by ID */
165
+ getAddon(addonId) {
166
+ return this.addons.get(addonId);
167
+ }
168
+ /** List all registered addons */
169
+ listAddons() {
170
+ return [...this.addons.values()];
171
+ }
172
+ /** Check if an addon is registered */
173
+ hasAddon(addonId) {
174
+ return this.addons.has(addonId);
175
+ }
176
+ /** Create a new instance of an addon (not yet initialized) */
177
+ createInstance(addonId) {
178
+ const registered = this.addons.get(addonId);
179
+ if (!registered) {
180
+ throw new Error(`Addon "${addonId}" is not registered`);
181
+ }
182
+ return new registered.addonClass();
183
+ }
184
+ };
203
185
 
204
- // src/addon-engine-manager.js
205
- var require_addon_engine_manager = __commonJS({
206
- "src/addon-engine-manager.js"(exports2) {
207
- "use strict";
208
- Object.defineProperty(exports2, "__esModule", { value: true });
209
- exports2.AddonEngineManager = void 0;
210
- var node_crypto_1 = require("crypto");
211
- var AddonEngineManager2 = class {
212
- loader;
213
- baseContext;
214
- engines = /* @__PURE__ */ new Map();
215
- constructor(loader, baseContext) {
216
- this.loader = loader;
217
- this.baseContext = baseContext;
218
- }
219
- /**
220
- * Get or create an addon engine for the given effective config.
221
- * Cameras with the same addonId + effective config share the same engine.
222
- */
223
- async getOrCreateEngine(addonId, globalConfig, cameraOverride) {
224
- const effectiveConfig = { ...globalConfig, ...cameraOverride };
225
- const configKey = `${addonId}:${this.hashConfig(effectiveConfig)}`;
226
- const existing = this.engines.get(configKey);
227
- if (existing)
228
- return existing;
229
- const addon = this.loader.createInstance(addonId);
230
- await addon.initialize({ ...this.baseContext, addonConfig: effectiveConfig });
231
- this.engines.set(configKey, addon);
232
- return addon;
233
- }
234
- /** Get all active engines */
235
- getActiveEngines() {
236
- return new Map(this.engines);
237
- }
238
- /** Shutdown a specific engine by its config key */
239
- async shutdownEngine(configKey) {
240
- const engine = this.engines.get(configKey);
241
- if (engine) {
242
- await engine.shutdown();
243
- this.engines.delete(configKey);
244
- }
245
- }
246
- /** Shutdown all engines */
247
- async shutdownAll() {
248
- for (const [, engine] of this.engines) {
249
- await engine.shutdown();
250
- }
251
- this.engines.clear();
252
- }
253
- /** Compute a deterministic config key (visible for tests) */
254
- computeConfigKey(addonId, effectiveConfig) {
255
- return `${addonId}:${this.hashConfig(effectiveConfig)}`;
256
- }
257
- hashConfig(config) {
258
- const sorted = JSON.stringify(config, Object.keys(config).sort());
259
- return (0, node_crypto_1.createHash)("md5").update(sorted).digest("hex").slice(0, 12);
260
- }
261
- };
262
- exports2.AddonEngineManager = AddonEngineManager2;
186
+ // src/addon-engine-manager.ts
187
+ var import_node_crypto = require("crypto");
188
+ var AddonEngineManager = class {
189
+ constructor(loader, baseContext) {
190
+ this.loader = loader;
191
+ this.baseContext = baseContext;
263
192
  }
264
- });
193
+ engines = /* @__PURE__ */ new Map();
194
+ /**
195
+ * Get or create an addon engine for the given effective config.
196
+ * Cameras with the same addonId + effective config share the same engine.
197
+ */
198
+ async getOrCreateEngine(addonId, globalConfig, cameraOverride) {
199
+ const effectiveConfig = { ...globalConfig, ...cameraOverride };
200
+ const configKey = `${addonId}:${this.hashConfig(effectiveConfig)}`;
201
+ const existing = this.engines.get(configKey);
202
+ if (existing) return existing;
203
+ const addon = this.loader.createInstance(addonId);
204
+ await addon.initialize({ ...this.baseContext, addonConfig: effectiveConfig });
205
+ this.engines.set(configKey, addon);
206
+ return addon;
207
+ }
208
+ /** Get all active engines */
209
+ getActiveEngines() {
210
+ return new Map(this.engines);
211
+ }
212
+ /** Shutdown a specific engine by its config key */
213
+ async shutdownEngine(configKey) {
214
+ const engine = this.engines.get(configKey);
215
+ if (engine) {
216
+ await engine.shutdown();
217
+ this.engines.delete(configKey);
218
+ }
219
+ }
220
+ /** Shutdown all engines */
221
+ async shutdownAll() {
222
+ for (const [, engine] of this.engines) {
223
+ await engine.shutdown();
224
+ }
225
+ this.engines.clear();
226
+ }
227
+ /** Compute a deterministic config key (visible for tests) */
228
+ computeConfigKey(addonId, effectiveConfig) {
229
+ return `${addonId}:${this.hashConfig(effectiveConfig)}`;
230
+ }
231
+ hashConfig(config) {
232
+ const sorted = JSON.stringify(config, Object.keys(config).sort());
233
+ return (0, import_node_crypto.createHash)("md5").update(sorted).digest("hex").slice(0, 12);
234
+ }
235
+ };
265
236
 
266
- // src/fs-utils.js
267
- var require_fs_utils = __commonJS({
268
- "src/fs-utils.js"(exports2) {
269
- "use strict";
270
- var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
271
- if (k2 === void 0) k2 = k;
272
- var desc = Object.getOwnPropertyDescriptor(m, k);
273
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
274
- desc = { enumerable: true, get: function() {
275
- return m[k];
276
- } };
277
- }
278
- Object.defineProperty(o, k2, desc);
279
- }) : (function(o, m, k, k2) {
280
- if (k2 === void 0) k2 = k;
281
- o[k2] = m[k];
282
- }));
283
- var __setModuleDefault = exports2 && exports2.__setModuleDefault || (Object.create ? (function(o, v) {
284
- Object.defineProperty(o, "default", { enumerable: true, value: v });
285
- }) : function(o, v) {
286
- o["default"] = v;
237
+ // src/addon-installer.ts
238
+ var import_node_child_process2 = require("child_process");
239
+ var import_node_util = require("util");
240
+ var fs3 = __toESM(require("fs"));
241
+ var path3 = __toESM(require("path"));
242
+ var os2 = __toESM(require("os"));
243
+
244
+ // src/fs-utils.ts
245
+ var import_node_child_process = require("child_process");
246
+ var fs2 = __toESM(require("fs"));
247
+ var os = __toESM(require("os"));
248
+ var path2 = __toESM(require("path"));
249
+ function ensureDir(dirPath) {
250
+ fs2.mkdirSync(dirPath, { recursive: true });
251
+ }
252
+ function copyDirRecursive(src, dest) {
253
+ ensureDir(dest);
254
+ const entries = fs2.readdirSync(src, { withFileTypes: true });
255
+ for (const entry of entries) {
256
+ const srcPath = path2.join(src, entry.name);
257
+ const destPath = path2.join(dest, entry.name);
258
+ if (entry.isDirectory()) {
259
+ copyDirRecursive(srcPath, destPath);
260
+ } else {
261
+ fs2.copyFileSync(srcPath, destPath);
262
+ }
263
+ }
264
+ }
265
+ function stripCamstackDeps(pkg) {
266
+ const result = { ...pkg };
267
+ for (const depType of ["dependencies", "peerDependencies", "devDependencies"]) {
268
+ const deps = result[depType];
269
+ if (deps) {
270
+ const filtered = {};
271
+ for (const [name, version] of Object.entries(deps)) {
272
+ if (!name.startsWith("@camstack/")) {
273
+ filtered[name] = version;
274
+ }
275
+ }
276
+ result[depType] = Object.keys(filtered).length > 0 ? filtered : void 0;
277
+ }
278
+ }
279
+ delete result.devDependencies;
280
+ return result;
281
+ }
282
+ function copyExtraFileDirs(pkgJson, sourceDir, destDir) {
283
+ const files = pkgJson.files;
284
+ if (!files) return;
285
+ for (const fileEntry of files) {
286
+ if (fileEntry === "dist" || fileEntry.includes("*")) continue;
287
+ const srcPath = path2.join(sourceDir, fileEntry);
288
+ if (!fs2.existsSync(srcPath)) continue;
289
+ const destPath = path2.join(destDir, fileEntry);
290
+ const stat = fs2.statSync(srcPath);
291
+ if (stat.isDirectory()) {
292
+ copyDirRecursive(srcPath, destPath);
293
+ } else if (stat.isFile()) {
294
+ ensureDir(path2.dirname(destPath));
295
+ fs2.copyFileSync(srcPath, destPath);
296
+ }
297
+ }
298
+ }
299
+ function symlinkAddonsToNodeModules(addonsDir, nodeModulesDir) {
300
+ const camstackDir = path2.join(nodeModulesDir, "@camstack");
301
+ ensureDir(camstackDir);
302
+ const packagesToLink = ["core"];
303
+ for (const pkg of packagesToLink) {
304
+ const addonDir = path2.join(addonsDir, "@camstack", pkg);
305
+ const linkPath = path2.join(camstackDir, pkg);
306
+ if (!fs2.existsSync(addonDir)) continue;
307
+ try {
308
+ const stat = fs2.lstatSync(linkPath);
309
+ if (stat.isSymbolicLink() || stat.isDirectory()) {
310
+ fs2.rmSync(linkPath, { recursive: true, force: true });
311
+ }
312
+ } catch {
313
+ }
314
+ fs2.symlinkSync(addonDir, linkPath, "dir");
315
+ console.log(`[symlink] node_modules/@camstack/${pkg} -> ${addonDir}`);
316
+ }
317
+ }
318
+ function isSourceNewer(packageDir) {
319
+ const srcDir = path2.join(packageDir, "src");
320
+ const distDir = path2.join(packageDir, "dist");
321
+ if (!fs2.existsSync(srcDir) || !fs2.existsSync(distDir)) return true;
322
+ try {
323
+ const distMtime = fs2.statSync(distDir).mtimeMs;
324
+ const entries = fs2.readdirSync(srcDir, { withFileTypes: true, recursive: true });
325
+ for (const entry of entries) {
326
+ if (!entry.isFile()) continue;
327
+ const filePath = path2.join(entry.parentPath ?? entry.path, entry.name);
328
+ if (fs2.statSync(filePath).mtimeMs > distMtime) return true;
329
+ }
330
+ return false;
331
+ } catch {
332
+ return true;
333
+ }
334
+ }
335
+ function ensureLibraryBuilt(packageName, packagesDir) {
336
+ const dirName = packageName.replace("@camstack/", "");
337
+ const sourceDir = path2.join(packagesDir, dirName);
338
+ if (!fs2.existsSync(sourceDir)) return;
339
+ const distDir = path2.join(sourceDir, "dist");
340
+ const hasIndex = fs2.existsSync(path2.join(distDir, "index.js")) || fs2.existsSync(path2.join(distDir, "index.mjs"));
341
+ if (hasIndex) return;
342
+ console.warn(`[ensureLibraryBuilt] ${packageName} has no dist/ \u2014 run 'npm run build' first`);
343
+ }
344
+ function installPackageFromNpmSync(packageName, targetDir) {
345
+ const tmpDir = fs2.mkdtempSync(path2.join(os.tmpdir(), "camstack-install-"));
346
+ try {
347
+ const stdout = (0, import_node_child_process.execFileSync)("npm", ["pack", packageName, "--pack-destination", tmpDir], {
348
+ timeout: 12e4,
349
+ encoding: "utf-8"
287
350
  });
288
- var __importStar = exports2 && exports2.__importStar || /* @__PURE__ */ (function() {
289
- var ownKeys = function(o) {
290
- ownKeys = Object.getOwnPropertyNames || function(o2) {
291
- var ar = [];
292
- for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
293
- return ar;
294
- };
295
- return ownKeys(o);
296
- };
297
- return function(mod) {
298
- if (mod && mod.__esModule) return mod;
299
- var result = {};
300
- if (mod != null) {
301
- for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
351
+ const tgzFilename = stdout.trim().split("\n").pop()?.trim();
352
+ if (!tgzFilename) throw new Error("npm pack produced no output");
353
+ const tgzPath = path2.join(tmpDir, tgzFilename);
354
+ const extractDir = path2.join(tmpDir, "extracted");
355
+ ensureDir(extractDir);
356
+ (0, import_node_child_process.execFileSync)("tar", ["-xzf", tgzPath, "-C", extractDir], { timeout: 3e4 });
357
+ const packageSubDir = path2.join(extractDir, "package");
358
+ const srcPkgJsonDir = fs2.existsSync(path2.join(packageSubDir, "package.json")) ? packageSubDir : extractDir;
359
+ fs2.rmSync(targetDir, { recursive: true, force: true });
360
+ ensureDir(targetDir);
361
+ fs2.copyFileSync(path2.join(srcPkgJsonDir, "package.json"), path2.join(targetDir, "package.json"));
362
+ const distSrc = path2.join(srcPkgJsonDir, "dist");
363
+ if (fs2.existsSync(distSrc)) {
364
+ copyDirRecursive(distSrc, path2.join(targetDir, "dist"));
365
+ }
366
+ try {
367
+ const npmPkg = JSON.parse(fs2.readFileSync(path2.join(srcPkgJsonDir, "package.json"), "utf-8"));
368
+ copyExtraFileDirs(npmPkg, srcPkgJsonDir, targetDir);
369
+ } catch {
370
+ }
371
+ } finally {
372
+ fs2.rmSync(tmpDir, { recursive: true, force: true });
373
+ }
374
+ }
375
+
376
+ // src/addon-installer.ts
377
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process2.execFile);
378
+ var AddonInstaller = class _AddonInstaller {
379
+ addonsDir;
380
+ registry;
381
+ workspacePackagesDir;
382
+ constructor(config) {
383
+ this.addonsDir = config.addonsDir;
384
+ this.registry = config.registry;
385
+ this.workspacePackagesDir = config.workspacePackagesDir;
386
+ }
387
+ /** Required addon packages that must be installed for the server to function */
388
+ static REQUIRED_PACKAGES = [
389
+ "@camstack/core",
390
+ "@camstack/addon-stream-broker",
391
+ "@camstack/addon-recording",
392
+ "@camstack/addon-vision",
393
+ "@camstack/addon-admin-ui",
394
+ "@camstack/addon-webrtc-adaptive",
395
+ "@camstack/addon-analytics",
396
+ "@camstack/addon-scene-intelligence",
397
+ "@camstack/addon-advanced-notifier"
398
+ ];
399
+ /** Ensure the addons directory exists */
400
+ async initialize() {
401
+ ensureDir(this.addonsDir);
402
+ }
403
+ /**
404
+ * Ensure all required packages are installed in the addons directory.
405
+ * This replaces the standalone first-boot-installer.ts.
406
+ */
407
+ async ensureRequiredPackages() {
408
+ ensureDir(this.addonsDir);
409
+ if (this.workspacePackagesDir) {
410
+ console.log(`[AddonInstaller] Workspace detected: ${this.workspacePackagesDir}`);
411
+ ensureLibraryBuilt("@camstack/kernel", this.workspacePackagesDir);
412
+ ensureLibraryBuilt("@camstack/types", this.workspacePackagesDir);
413
+ }
414
+ for (const packageName of _AddonInstaller.REQUIRED_PACKAGES) {
415
+ const addonDir = path3.join(this.addonsDir, packageName);
416
+ const pkgJsonPath = path3.join(addonDir, "package.json");
417
+ const forceNpm = process.env["CAMSTACK_INSTALL_SOURCE"] === "npm";
418
+ const useWorkspace = this.workspacePackagesDir && !forceNpm;
419
+ if (fs3.existsSync(pkgJsonPath)) {
420
+ if (!useWorkspace) {
421
+ console.log(`[AddonInstaller] ${packageName} \u2014 already installed (npm), skipping`);
422
+ continue;
302
423
  }
303
- __setModuleDefault(result, mod);
304
- return result;
305
- };
306
- })();
307
- Object.defineProperty(exports2, "__esModule", { value: true });
308
- exports2.ensureDir = ensureDir2;
309
- exports2.copyDirRecursive = copyDirRecursive2;
310
- exports2.stripCamstackDeps = stripCamstackDeps2;
311
- exports2.copyExtraFileDirs = copyExtraFileDirs2;
312
- exports2.symlinkAddonsToNodeModules = symlinkAddonsToNodeModules2;
313
- exports2.isSourceNewer = isSourceNewer2;
314
- exports2.ensureLibraryBuilt = ensureLibraryBuilt2;
315
- exports2.installPackageFromNpmSync = installPackageFromNpmSync2;
316
- var node_child_process_1 = require("child_process");
317
- var fs = __importStar(require("fs"));
318
- var path = __importStar(require("path"));
319
- function ensureDir2(dirPath) {
320
- fs.mkdirSync(dirPath, { recursive: true });
321
- }
322
- function copyDirRecursive2(src, dest) {
323
- ensureDir2(dest);
324
- const entries = fs.readdirSync(src, { withFileTypes: true });
325
- for (const entry of entries) {
326
- const srcPath = path.join(src, entry.name);
327
- const destPath = path.join(dest, entry.name);
328
- if (entry.isDirectory()) {
329
- copyDirRecursive2(srcPath, destPath);
330
- } else {
331
- fs.copyFileSync(srcPath, destPath);
424
+ const srcPkgDir = this.findWorkspacePackage(packageName);
425
+ if (srcPkgDir && !isSourceNewer(srcPkgDir)) {
426
+ console.log(`[AddonInstaller] ${packageName} \u2014 workspace up-to-date, skipping`);
427
+ continue;
332
428
  }
333
429
  }
334
- }
335
- function stripCamstackDeps2(pkg) {
336
- const result = { ...pkg };
337
- for (const depType of ["dependencies", "peerDependencies", "devDependencies"]) {
338
- const deps = result[depType];
339
- if (deps) {
340
- const filtered = {};
341
- for (const [name, version] of Object.entries(deps)) {
342
- if (!name.startsWith("@camstack/")) {
343
- filtered[name] = version;
344
- }
430
+ try {
431
+ if (useWorkspace) {
432
+ const pkgDir = this.findWorkspacePackage(packageName);
433
+ if (pkgDir) {
434
+ console.log(`[AddonInstaller] ${packageName} \u2014 installing from workspace`);
435
+ await this.installFromWorkspace(packageName);
436
+ continue;
345
437
  }
346
- result[depType] = Object.keys(filtered).length > 0 ? filtered : void 0;
347
438
  }
348
- }
349
- delete result.devDependencies;
350
- return result;
351
- }
352
- function copyExtraFileDirs2(pkgJson, sourceDir, destDir) {
353
- const files = pkgJson.files;
354
- if (!files)
355
- return;
356
- for (const fileEntry of files) {
357
- if (fileEntry === "dist" || fileEntry.includes("*"))
358
- continue;
359
- const srcPath = path.join(sourceDir, fileEntry);
360
- if (!fs.existsSync(srcPath))
361
- continue;
362
- const destPath = path.join(destDir, fileEntry);
363
- const stat = fs.statSync(srcPath);
364
- if (stat.isDirectory()) {
365
- copyDirRecursive2(srcPath, destPath);
366
- } else if (stat.isFile()) {
367
- ensureDir2(path.dirname(destPath));
368
- fs.copyFileSync(srcPath, destPath);
439
+ console.log(`[AddonInstaller] ${packageName} \u2014 installing from npm`);
440
+ await this.installFromNpm(packageName);
441
+ } catch (err) {
442
+ const msg = err instanceof Error ? err.message : String(err);
443
+ if (packageName === "@camstack/core") {
444
+ throw new Error(`Required package ${packageName} failed to install: ${msg}`);
369
445
  }
446
+ console.error(`[AddonInstaller] Failed to install ${packageName}: ${msg}`);
370
447
  }
371
448
  }
372
- function symlinkAddonsToNodeModules2(addonsDir, nodeModulesDir) {
373
- const camstackDir = path.join(nodeModulesDir, "@camstack");
374
- ensureDir2(camstackDir);
375
- const packagesToLink = ["core"];
376
- for (const pkg of packagesToLink) {
377
- const addonDir = path.join(addonsDir, "@camstack", pkg);
378
- const linkPath = path.join(camstackDir, pkg);
379
- if (!fs.existsSync(addonDir))
380
- continue;
449
+ }
450
+ findWorkspacePackage(packageName) {
451
+ if (!this.workspacePackagesDir) return null;
452
+ const shortName = packageName.replace("@camstack/", "");
453
+ for (const dirName of [shortName, `addon-${shortName}`, shortName.replace("addon-", "")]) {
454
+ const candidate = path3.join(this.workspacePackagesDir, dirName);
455
+ if (fs3.existsSync(path3.join(candidate, "package.json"))) {
381
456
  try {
382
- const stat = fs.lstatSync(linkPath);
383
- if (stat.isSymbolicLink() || stat.isDirectory()) {
384
- fs.rmSync(linkPath, { recursive: true, force: true });
385
- }
457
+ const pkg = JSON.parse(fs3.readFileSync(path3.join(candidate, "package.json"), "utf-8"));
458
+ if (pkg.name === packageName) return candidate;
386
459
  } catch {
387
460
  }
388
- fs.symlinkSync(addonDir, linkPath, "dir");
389
- console.log(`[symlink] node_modules/@camstack/${pkg} -> ${addonDir}`);
390
461
  }
391
462
  }
392
- function isSourceNewer2(packageDir) {
393
- const srcDir = path.join(packageDir, "src");
394
- const distDir = path.join(packageDir, "dist");
395
- if (!fs.existsSync(srcDir) || !fs.existsSync(distDir))
396
- return true;
463
+ return null;
464
+ }
465
+ /** Install addon from a tgz file (uploaded or downloaded) */
466
+ async installFromTgz(tgzPath) {
467
+ const tmpDir = fs3.mkdtempSync(path3.join(os2.tmpdir(), "camstack-addon-install-"));
468
+ try {
469
+ await execFileAsync("tar", ["-xzf", tgzPath, "-C", tmpDir], { timeout: 3e4 });
470
+ const extractedDir = path3.join(tmpDir, "package");
471
+ const pkgJsonPath = fs3.existsSync(path3.join(extractedDir, "package.json")) ? path3.join(extractedDir, "package.json") : path3.join(tmpDir, "package.json");
472
+ if (!fs3.existsSync(pkgJsonPath)) {
473
+ throw new Error("No package.json found in tgz archive");
474
+ }
475
+ const pkgJson = JSON.parse(fs3.readFileSync(pkgJsonPath, "utf-8"));
476
+ if (!pkgJson.camstack?.addons) {
477
+ throw new Error(`Package ${pkgJson.name} has no camstack.addons manifest`);
478
+ }
479
+ const targetDir = path3.join(this.addonsDir, pkgJson.name);
480
+ fs3.rmSync(targetDir, { recursive: true, force: true });
481
+ ensureDir(targetDir);
482
+ const sourceDir = path3.dirname(pkgJsonPath);
483
+ fs3.copyFileSync(pkgJsonPath, path3.join(targetDir, "package.json"));
484
+ const sourceDist = path3.join(sourceDir, "dist");
485
+ if (fs3.existsSync(sourceDist)) {
486
+ copyDirRecursive(sourceDist, path3.join(targetDir, "dist"));
487
+ }
488
+ copyExtraFileDirs(pkgJson, sourceDir, targetDir);
489
+ return { name: pkgJson.name, version: pkgJson.version };
490
+ } finally {
491
+ fs3.rmSync(tmpDir, { recursive: true, force: true });
492
+ }
493
+ }
494
+ /**
495
+ * Install addon — prefers workspace if available, falls back to npm.
496
+ * This ensures dev builds (with vite output, etc.) are used when in workspace mode.
497
+ */
498
+ async install(packageName, version) {
499
+ if (this.workspacePackagesDir) {
500
+ const workspaceDirName = packageName.replace("@camstack/", "");
501
+ const sourceDir = path3.join(this.workspacePackagesDir, workspaceDirName);
502
+ if (fs3.existsSync(path3.join(sourceDir, "package.json"))) {
503
+ return this.installFromWorkspace(packageName);
504
+ }
505
+ }
506
+ return this.installFromNpm(packageName, version);
507
+ }
508
+ /**
509
+ * Install addon from workspace by copying package.json + dist/ to addons dir.
510
+ * Does NOT build — expects dist/ to already exist (run `npm run build` separately).
511
+ */
512
+ async installFromWorkspace(packageName) {
513
+ if (!this.workspacePackagesDir) {
514
+ throw new Error("Workspace packages directory not configured");
515
+ }
516
+ const workspaceDirName = packageName.replace("@camstack/", "");
517
+ const sourceDir = path3.join(this.workspacePackagesDir, workspaceDirName);
518
+ const sourcePkgJson = path3.join(sourceDir, "package.json");
519
+ if (!fs3.existsSync(sourcePkgJson)) {
520
+ throw new Error(`Workspace package not found: ${sourceDir}`);
521
+ }
522
+ const distDir = path3.join(sourceDir, "dist");
523
+ if (!fs3.existsSync(distDir)) {
524
+ throw new Error(`${packageName} has no dist/ directory \u2014 run 'npm run build' first`);
525
+ }
526
+ const targetDir = path3.join(this.addonsDir, packageName);
527
+ fs3.rmSync(targetDir, { recursive: true, force: true });
528
+ ensureDir(targetDir);
529
+ const pkgData = JSON.parse(fs3.readFileSync(sourcePkgJson, "utf-8"));
530
+ const strippedPkg = stripCamstackDeps(pkgData);
531
+ fs3.writeFileSync(path3.join(targetDir, "package.json"), JSON.stringify(strippedPkg, null, 2));
532
+ copyDirRecursive(distDir, path3.join(targetDir, "dist"));
533
+ copyExtraFileDirs(pkgData, sourceDir, targetDir);
534
+ fs3.writeFileSync(path3.join(targetDir, ".install-source"), "workspace");
535
+ try {
536
+ await execFileAsync("npm", ["install", "--omit=dev", "--ignore-scripts=false"], {
537
+ cwd: targetDir,
538
+ timeout: 12e4
539
+ });
540
+ } catch {
541
+ }
542
+ return { name: pkgData.name, version: pkgData.version };
543
+ }
544
+ /** Install addon from npm (download tgz, then extract) */
545
+ async installFromNpm(packageName, version) {
546
+ const tmpDir = fs3.mkdtempSync(path3.join(os2.tmpdir(), "camstack-addon-npm-"));
547
+ const packageSpec = version ? `${packageName}@${version}` : packageName;
548
+ const args = ["pack", packageSpec, "--pack-destination", tmpDir];
549
+ if (this.registry) {
550
+ args.push("--registry", this.registry);
551
+ }
552
+ console.log(`[AddonInstaller] npm pack ${packageSpec} \u2192 ${tmpDir}`);
553
+ try {
554
+ const { stdout } = await execFileAsync("npm", args, {
555
+ timeout: 12e4
556
+ });
557
+ const tgzFiles = fs3.readdirSync(tmpDir).filter((f) => f.endsWith(".tgz"));
558
+ console.log(`[AddonInstaller] npm pack stdout: ${stdout.trim()}, tgzFiles: ${tgzFiles.join(", ")}`);
559
+ if (tgzFiles.length === 0) {
560
+ throw new Error(`npm pack produced no tgz file for ${packageSpec}. stdout: ${stdout.trim()}`);
561
+ }
562
+ const tgzPath = path3.join(tmpDir, tgzFiles[0]);
563
+ const result = await this.installFromTgz(tgzPath);
564
+ console.log(`[AddonInstaller] installFromTgz result: ${result.name}@${result.version} \u2192 ${path3.join(this.addonsDir, result.name)}`);
565
+ const targetDir = path3.join(this.addonsDir, result.name);
566
+ fs3.writeFileSync(path3.join(targetDir, ".install-source"), "npm");
567
+ return result;
568
+ } finally {
569
+ fs3.rmSync(tmpDir, { recursive: true, force: true });
570
+ }
571
+ }
572
+ /** Uninstall addon (delete directory) */
573
+ async uninstall(packageName) {
574
+ const addonDir = path3.join(this.addonsDir, packageName);
575
+ if (fs3.existsSync(addonDir)) {
576
+ fs3.rmSync(addonDir, { recursive: true, force: true });
577
+ return;
578
+ }
579
+ const legacyDir = path3.join(this.addonsDir, packageName.replace(/^@[^/]+\//, ""));
580
+ if (fs3.existsSync(legacyDir)) {
581
+ fs3.rmSync(legacyDir, { recursive: true, force: true });
582
+ }
583
+ }
584
+ /** List installed addons (directories with package.json containing camstack.addons) */
585
+ listInstalled() {
586
+ if (!fs3.existsSync(this.addonsDir)) return [];
587
+ const packageDirs = [];
588
+ for (const entry of fs3.readdirSync(this.addonsDir, { withFileTypes: true })) {
589
+ if (!entry.isDirectory()) continue;
590
+ if (entry.name.startsWith("@")) {
591
+ const scopeDir = path3.join(this.addonsDir, entry.name);
592
+ for (const inner of fs3.readdirSync(scopeDir, { withFileTypes: true })) {
593
+ if (inner.isDirectory()) packageDirs.push(path3.join(scopeDir, inner.name));
594
+ }
595
+ } else {
596
+ packageDirs.push(path3.join(this.addonsDir, entry.name));
597
+ }
598
+ }
599
+ return packageDirs.map((dir) => {
600
+ const pkgPath = path3.join(dir, "package.json");
601
+ if (!fs3.existsSync(pkgPath)) return null;
397
602
  try {
398
- const distMtime = fs.statSync(distDir).mtimeMs;
399
- const entries = fs.readdirSync(srcDir, { withFileTypes: true, recursive: true });
400
- for (const entry of entries) {
401
- if (!entry.isFile())
402
- continue;
403
- const filePath = path.join(entry.parentPath ?? entry.path, entry.name);
404
- if (fs.statSync(filePath).mtimeMs > distMtime)
405
- return true;
406
- }
407
- return false;
603
+ const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
604
+ if (!pkg.camstack?.addons) return null;
605
+ const sourceFile = path3.join(dir, ".install-source");
606
+ const installSource = fs3.existsSync(sourceFile) ? fs3.readFileSync(sourceFile, "utf-8").trim() : void 0;
607
+ const result = { name: pkg.name, version: pkg.version, dir, ...installSource ? { installSource } : {} };
608
+ return result;
408
609
  } catch {
409
- return true;
610
+ return null;
410
611
  }
612
+ }).filter((item) => item !== null);
613
+ }
614
+ /** Check if an addon is installed */
615
+ isInstalled(packageName) {
616
+ if (fs3.existsSync(path3.join(this.addonsDir, packageName, "package.json"))) return true;
617
+ const legacy = packageName.replace(/^@[^/]+\//, "");
618
+ return fs3.existsSync(path3.join(this.addonsDir, legacy, "package.json"));
619
+ }
620
+ /** Get installed package info */
621
+ getInstalledPackage(packageName) {
622
+ let pkgPath = path3.join(this.addonsDir, packageName, "package.json");
623
+ if (!fs3.existsSync(pkgPath)) {
624
+ pkgPath = path3.join(this.addonsDir, packageName.replace(/^@[^/]+\//, ""), "package.json");
625
+ }
626
+ if (!fs3.existsSync(pkgPath)) return null;
627
+ try {
628
+ const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
629
+ return { name: pkg.name, version: pkg.version, dir: path3.dirname(pkgPath) };
630
+ } catch {
631
+ return null;
411
632
  }
412
- function ensureLibraryBuilt2(packageName, packagesDir) {
413
- const dirName = packageName.replace("@camstack/", "");
414
- const sourceDir = path.join(packagesDir, dirName);
415
- if (!fs.existsSync(sourceDir))
416
- return;
417
- const distDir = path.join(sourceDir, "dist");
418
- const hasIndex = fs.existsSync(path.join(distDir, "index.js")) || fs.existsSync(path.join(distDir, "index.mjs"));
419
- if (hasIndex)
420
- return;
421
- console.warn(`[ensureLibraryBuilt] ${packageName} has no dist/ \u2014 run 'npm run build' first`);
422
- }
423
- function installPackageFromNpmSync2(packageName, targetDir) {
424
- const os = require("os");
425
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "camstack-install-"));
633
+ }
634
+ };
635
+
636
+ // src/workspace-detect.ts
637
+ var fs4 = __toESM(require("fs"));
638
+ var path4 = __toESM(require("path"));
639
+ function detectWorkspacePackagesDir(startDir) {
640
+ let current = path4.resolve(startDir);
641
+ for (let i = 0; i < 6; i++) {
642
+ current = path4.dirname(current);
643
+ const packagesDir = path4.join(current, "packages");
644
+ const rootPkgJson = path4.join(current, "package.json");
645
+ if (fs4.existsSync(packagesDir) && fs4.existsSync(rootPkgJson)) {
426
646
  try {
427
- const stdout = (0, node_child_process_1.execFileSync)("npm", ["pack", packageName, "--pack-destination", tmpDir], {
428
- timeout: 12e4,
429
- encoding: "utf-8"
430
- });
431
- const tgzFilename = stdout.trim().split("\n").pop()?.trim();
432
- if (!tgzFilename)
433
- throw new Error("npm pack produced no output");
434
- const tgzPath = path.join(tmpDir, tgzFilename);
435
- const extractDir = path.join(tmpDir, "extracted");
436
- ensureDir2(extractDir);
437
- (0, node_child_process_1.execFileSync)("tar", ["-xzf", tgzPath, "-C", extractDir], { timeout: 3e4 });
438
- const packageSubDir = path.join(extractDir, "package");
439
- const srcPkgJsonDir = fs.existsSync(path.join(packageSubDir, "package.json")) ? packageSubDir : extractDir;
440
- fs.rmSync(targetDir, { recursive: true, force: true });
441
- ensureDir2(targetDir);
442
- fs.copyFileSync(path.join(srcPkgJsonDir, "package.json"), path.join(targetDir, "package.json"));
443
- const distSrc = path.join(srcPkgJsonDir, "dist");
444
- if (fs.existsSync(distSrc)) {
445
- copyDirRecursive2(distSrc, path.join(targetDir, "dist"));
446
- }
447
- try {
448
- const npmPkg = JSON.parse(fs.readFileSync(path.join(srcPkgJsonDir, "package.json"), "utf-8"));
449
- copyExtraFileDirs2(npmPkg, srcPkgJsonDir, targetDir);
450
- } catch {
647
+ const rootPkg = JSON.parse(fs4.readFileSync(rootPkgJson, "utf-8"));
648
+ if (rootPkg.workspaces || rootPkg.name === "camstack-server" || rootPkg.name === "camstack") {
649
+ return packagesDir;
451
650
  }
452
- } finally {
453
- fs.rmSync(tmpDir, { recursive: true, force: true });
651
+ } catch {
454
652
  }
455
653
  }
456
654
  }
457
- });
655
+ return null;
656
+ }
458
657
 
459
- // src/addon-installer.js
460
- var require_addon_installer = __commonJS({
461
- "src/addon-installer.js"(exports2) {
462
- "use strict";
463
- var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
464
- if (k2 === void 0) k2 = k;
465
- var desc = Object.getOwnPropertyDescriptor(m, k);
466
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
467
- desc = { enumerable: true, get: function() {
468
- return m[k];
469
- } };
470
- }
471
- Object.defineProperty(o, k2, desc);
472
- }) : (function(o, m, k, k2) {
473
- if (k2 === void 0) k2 = k;
474
- o[k2] = m[k];
475
- }));
476
- var __setModuleDefault = exports2 && exports2.__setModuleDefault || (Object.create ? (function(o, v) {
477
- Object.defineProperty(o, "default", { enumerable: true, value: v });
478
- }) : function(o, v) {
479
- o["default"] = v;
658
+ // src/capability-registry.ts
659
+ var CapabilityRegistry = class {
660
+ constructor(logger, configReader) {
661
+ this.logger = logger;
662
+ this.configReader = configReader;
663
+ }
664
+ capabilities = /* @__PURE__ */ new Map();
665
+ /** Per-device singleton overrides: deviceId (capability addonId) */
666
+ deviceOverrides = /* @__PURE__ */ new Map();
667
+ /** Per-device collection filters: deviceId → (capability → addonIds[]) */
668
+ deviceCollectionFilters = /* @__PURE__ */ new Map();
669
+ /**
670
+ * Declare a capability (typically called when addon manifests are loaded).
671
+ * Must be called before registerProvider/registerConsumer for that capability.
672
+ */
673
+ declareCapability(declaration) {
674
+ if (this.capabilities.has(declaration.name)) {
675
+ this.logger.debug(`Capability "${declaration.name}" already declared, skipping`);
676
+ return;
677
+ }
678
+ this.capabilities.set(declaration.name, {
679
+ declaration,
680
+ available: /* @__PURE__ */ new Map(),
681
+ activeAddonId: null,
682
+ activeProvider: null,
683
+ activeCollection: [],
684
+ consumers: /* @__PURE__ */ new Set()
480
685
  });
481
- var __importStar = exports2 && exports2.__importStar || /* @__PURE__ */ (function() {
482
- var ownKeys = function(o) {
483
- ownKeys = Object.getOwnPropertyNames || function(o2) {
484
- var ar = [];
485
- for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
486
- return ar;
487
- };
488
- return ownKeys(o);
489
- };
490
- return function(mod) {
491
- if (mod && mod.__esModule) return mod;
492
- var result = {};
493
- if (mod != null) {
494
- for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
495
- }
496
- __setModuleDefault(result, mod);
497
- return result;
498
- };
499
- })();
500
- Object.defineProperty(exports2, "__esModule", { value: true });
501
- exports2.AddonInstaller = void 0;
502
- var node_child_process_1 = require("child_process");
503
- var node_util_1 = require("util");
504
- var fs = __importStar(require("fs"));
505
- var path = __importStar(require("path"));
506
- var os = __importStar(require("os"));
507
- var fs_utils_js_1 = require_fs_utils();
508
- var execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
509
- var AddonInstaller2 = class _AddonInstaller {
510
- addonsDir;
511
- registry;
512
- workspacePackagesDir;
513
- constructor(config) {
514
- this.addonsDir = config.addonsDir;
515
- this.registry = config.registry;
516
- this.workspacePackagesDir = config.workspacePackagesDir;
517
- }
518
- /** Required addon packages that must be installed for the server to function */
519
- static REQUIRED_PACKAGES = [
520
- "@camstack/core",
521
- "@camstack/addon-pipeline",
522
- "@camstack/addon-vision",
523
- "@camstack/addon-admin-ui",
524
- "@camstack/addon-webrtc-adaptive",
525
- "@camstack/addon-pipeline-analysis",
526
- "@camstack/addon-scene-intelligence",
527
- "@camstack/addon-advanced-notifier"
528
- ];
529
- /** Ensure the addons directory exists */
530
- async initialize() {
531
- (0, fs_utils_js_1.ensureDir)(this.addonsDir);
532
- }
533
- /**
534
- * Ensure all required packages are installed in the addons directory.
535
- * This replaces the standalone first-boot-installer.ts.
536
- */
537
- async ensureRequiredPackages() {
538
- (0, fs_utils_js_1.ensureDir)(this.addonsDir);
539
- if (this.workspacePackagesDir) {
540
- console.log(`[AddonInstaller] Workspace detected: ${this.workspacePackagesDir}`);
541
- (0, fs_utils_js_1.ensureLibraryBuilt)("@camstack/kernel", this.workspacePackagesDir);
542
- (0, fs_utils_js_1.ensureLibraryBuilt)("@camstack/types", this.workspacePackagesDir);
543
- }
544
- for (const packageName of _AddonInstaller.REQUIRED_PACKAGES) {
545
- const addonDir = path.join(this.addonsDir, packageName);
546
- const pkgJsonPath = path.join(addonDir, "package.json");
547
- const forceNpm = process.env["CAMSTACK_INSTALL_SOURCE"] === "npm";
548
- const useWorkspace = this.workspacePackagesDir && !forceNpm;
549
- if (fs.existsSync(pkgJsonPath)) {
550
- if (!useWorkspace) {
551
- continue;
552
- }
553
- const srcPkgDir = this.findWorkspacePackage(packageName);
554
- if (srcPkgDir && !(0, fs_utils_js_1.isSourceNewer)(srcPkgDir)) {
555
- continue;
556
- }
557
- }
686
+ this.logger.debug(`Capability declared: ${declaration.name} (mode=${declaration.mode})`);
687
+ }
688
+ /**
689
+ * Register a capability provider (called by addon loader when addon is enabled).
690
+ * For singleton: auto-activates if user-preferred or first registered.
691
+ * For collection: adds to active set and notifies consumers.
692
+ */
693
+ registerProvider(capability, addonId, provider) {
694
+ const state = this.capabilities.get(capability);
695
+ if (!state) {
696
+ this.logger.warn(`Cannot register provider for undeclared capability "${capability}"`);
697
+ return;
698
+ }
699
+ state.available.set(addonId, provider);
700
+ this.logger.info(`Provider registered: ${addonId} \u2192 ${capability}`);
701
+ if (state.declaration.mode === "singleton") {
702
+ const userChoice = this.configReader(capability);
703
+ if (userChoice === addonId) {
704
+ this.activateSingleton(state, addonId, provider);
705
+ } else if (userChoice === void 0 && state.activeAddonId === null) {
706
+ this.activateSingleton(state, addonId, provider);
707
+ }
708
+ } else {
709
+ state.activeCollection.push({ addonId, provider });
710
+ for (const consumer of state.consumers) {
711
+ if (consumer.onAdded) {
558
712
  try {
559
- if (useWorkspace) {
560
- const pkgDir = this.findWorkspacePackage(packageName);
561
- if (pkgDir) {
562
- await this.installFromWorkspace(packageName);
563
- continue;
564
- }
565
- }
566
- await this.installFromNpm(packageName);
567
- } catch (err) {
568
- const msg = err instanceof Error ? err.message : String(err);
569
- if (packageName === "@camstack/core") {
570
- throw new Error(`Required package ${packageName} failed to install: ${msg}`);
571
- }
572
- console.error(`[AddonInstaller] Failed to install ${packageName}: ${msg}`);
713
+ consumer.onAdded(provider);
714
+ } catch (error) {
715
+ const msg = error instanceof Error ? error.message : String(error);
716
+ this.logger.error(`Consumer onAdded failed for ${capability}: ${msg}`);
573
717
  }
574
718
  }
575
719
  }
576
- findWorkspacePackage(packageName) {
577
- if (!this.workspacePackagesDir)
578
- return null;
579
- const shortName = packageName.replace("@camstack/", "");
580
- for (const dirName of [shortName, `addon-${shortName}`, shortName.replace("addon-", "")]) {
581
- const candidate = path.join(this.workspacePackagesDir, dirName);
582
- if (fs.existsSync(path.join(candidate, "package.json"))) {
720
+ }
721
+ }
722
+ /**
723
+ * Unregister a provider (called when addon is disabled/uninstalled).
724
+ */
725
+ unregisterProvider(capability, addonId) {
726
+ const state = this.capabilities.get(capability);
727
+ if (!state) return;
728
+ const provider = state.available.get(addonId);
729
+ state.available.delete(addonId);
730
+ if (state.declaration.mode === "singleton") {
731
+ if (state.activeAddonId === addonId) {
732
+ state.activeAddonId = null;
733
+ state.activeProvider = null;
734
+ this.logger.info(`Singleton deactivated: ${capability} (was ${addonId})`);
735
+ }
736
+ } else {
737
+ const idx = state.activeCollection.findIndex((e) => e.addonId === addonId);
738
+ if (idx !== -1) {
739
+ state.activeCollection.splice(idx, 1);
740
+ for (const consumer of state.consumers) {
741
+ if (consumer.onRemoved && provider !== void 0) {
583
742
  try {
584
- const pkg = JSON.parse(fs.readFileSync(path.join(candidate, "package.json"), "utf-8"));
585
- if (pkg.name === packageName)
586
- return candidate;
587
- } catch {
743
+ consumer.onRemoved(provider);
744
+ } catch (error) {
745
+ const msg = error instanceof Error ? error.message : String(error);
746
+ this.logger.error(`Consumer onRemoved failed for ${capability}: ${msg}`);
588
747
  }
589
748
  }
590
749
  }
591
- return null;
592
- }
593
- /** Install addon from a tgz file (uploaded or downloaded) */
594
- async installFromTgz(tgzPath) {
595
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "camstack-addon-install-"));
596
- try {
597
- await execFileAsync("tar", ["-xzf", tgzPath, "-C", tmpDir], { timeout: 3e4 });
598
- const extractedDir = path.join(tmpDir, "package");
599
- const pkgJsonPath = fs.existsSync(path.join(extractedDir, "package.json")) ? path.join(extractedDir, "package.json") : path.join(tmpDir, "package.json");
600
- if (!fs.existsSync(pkgJsonPath)) {
601
- throw new Error("No package.json found in tgz archive");
602
- }
603
- const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
604
- if (!pkgJson.camstack?.addons) {
605
- throw new Error(`Package ${pkgJson.name} has no camstack.addons manifest`);
606
- }
607
- const targetDir = path.join(this.addonsDir, pkgJson.name);
608
- fs.rmSync(targetDir, { recursive: true, force: true });
609
- (0, fs_utils_js_1.ensureDir)(targetDir);
610
- const sourceDir = path.dirname(pkgJsonPath);
611
- fs.copyFileSync(pkgJsonPath, path.join(targetDir, "package.json"));
612
- const sourceDist = path.join(sourceDir, "dist");
613
- if (fs.existsSync(sourceDist)) {
614
- (0, fs_utils_js_1.copyDirRecursive)(sourceDist, path.join(targetDir, "dist"));
615
- }
616
- (0, fs_utils_js_1.copyExtraFileDirs)(pkgJson, sourceDir, targetDir);
617
- return { name: pkgJson.name, version: pkgJson.version };
618
- } finally {
619
- fs.rmSync(tmpDir, { recursive: true, force: true });
620
- }
621
- }
622
- /**
623
- * Install addon — prefers workspace if available, falls back to npm.
624
- * This ensures dev builds (with vite output, etc.) are used when in workspace mode.
625
- */
626
- async install(packageName, version) {
627
- if (this.workspacePackagesDir) {
628
- const workspaceDirName = packageName.replace("@camstack/", "");
629
- const sourceDir = path.join(this.workspacePackagesDir, workspaceDirName);
630
- if (fs.existsSync(path.join(sourceDir, "package.json"))) {
631
- return this.installFromWorkspace(packageName);
632
- }
633
- }
634
- return this.installFromNpm(packageName, version);
635
- }
636
- /**
637
- * Install addon from workspace by copying package.json + dist/ to addons dir.
638
- * Does NOT build — expects dist/ to already exist (run `npm run build` separately).
639
- */
640
- async installFromWorkspace(packageName) {
641
- if (!this.workspacePackagesDir) {
642
- throw new Error("Workspace packages directory not configured");
643
- }
644
- const workspaceDirName = packageName.replace("@camstack/", "");
645
- const sourceDir = path.join(this.workspacePackagesDir, workspaceDirName);
646
- const sourcePkgJson = path.join(sourceDir, "package.json");
647
- if (!fs.existsSync(sourcePkgJson)) {
648
- throw new Error(`Workspace package not found: ${sourceDir}`);
649
- }
650
- const distDir = path.join(sourceDir, "dist");
651
- if (!fs.existsSync(distDir)) {
652
- throw new Error(`${packageName} has no dist/ directory \u2014 run 'npm run build' first`);
653
- }
654
- const targetDir = path.join(this.addonsDir, packageName);
655
- fs.rmSync(targetDir, { recursive: true, force: true });
656
- (0, fs_utils_js_1.ensureDir)(targetDir);
657
- const pkgData = JSON.parse(fs.readFileSync(sourcePkgJson, "utf-8"));
658
- const strippedPkg = (0, fs_utils_js_1.stripCamstackDeps)(pkgData);
659
- fs.writeFileSync(path.join(targetDir, "package.json"), JSON.stringify(strippedPkg, null, 2));
660
- (0, fs_utils_js_1.copyDirRecursive)(distDir, path.join(targetDir, "dist"));
661
- (0, fs_utils_js_1.copyExtraFileDirs)(pkgData, sourceDir, targetDir);
662
- try {
663
- await execFileAsync("npm", ["install", "--omit=dev", "--ignore-scripts=false"], {
664
- cwd: targetDir,
665
- timeout: 12e4
666
- });
667
- } catch {
668
- }
669
- return { name: pkgData.name, version: pkgData.version };
670
750
  }
671
- /** Install addon from npm (download tgz, then extract) */
672
- async installFromNpm(packageName, version) {
673
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "camstack-addon-npm-"));
674
- const packageSpec = version ? `${packageName}@${version}` : packageName;
675
- const args = ["pack", packageSpec, "--pack-destination", tmpDir];
676
- if (this.registry) {
677
- args.push("--registry", this.registry);
678
- }
751
+ }
752
+ }
753
+ /**
754
+ * Register a consumer that wants to be notified when providers change.
755
+ * If a provider is already active, the consumer is immediately notified.
756
+ * Returns a disposer function for cleanup.
757
+ */
758
+ registerConsumer(registration) {
759
+ const state = this.capabilities.get(registration.capability);
760
+ if (!state) {
761
+ this.logger.debug(`Consumer registered for undeclared capability "${registration.capability}" \u2014 auto-declaring`);
762
+ this.declareCapability({ name: registration.capability, mode: "singleton" });
763
+ return this.registerConsumer(registration);
764
+ }
765
+ const untypedReg = registration;
766
+ state.consumers.add(untypedReg);
767
+ if (state.declaration.mode === "singleton") {
768
+ if (state.activeProvider !== null && registration.onSet) {
679
769
  try {
680
- const { stdout } = await execFileAsync("npm", args, {
681
- timeout: 12e4
682
- });
683
- const tgzFiles = fs.readdirSync(tmpDir).filter((f) => f.endsWith(".tgz"));
684
- if (tgzFiles.length === 0) {
685
- throw new Error(`npm pack produced no tgz file for ${packageSpec}. stdout: ${stdout.trim()}`);
686
- }
687
- const tgzPath = path.join(tmpDir, tgzFiles[0]);
688
- const result = await this.installFromTgz(tgzPath);
689
- return result;
690
- } finally {
691
- fs.rmSync(tmpDir, { recursive: true, force: true });
692
- }
693
- }
694
- /** Uninstall addon (delete directory) */
695
- async uninstall(packageName) {
696
- const addonDir = path.join(this.addonsDir, packageName);
697
- if (fs.existsSync(addonDir)) {
698
- fs.rmSync(addonDir, { recursive: true, force: true });
699
- return;
700
- }
701
- const legacyDir = path.join(this.addonsDir, packageName.replace(/^@[^/]+\//, ""));
702
- if (fs.existsSync(legacyDir)) {
703
- fs.rmSync(legacyDir, { recursive: true, force: true });
770
+ registration.onSet(state.activeProvider);
771
+ } catch (error) {
772
+ const msg = error instanceof Error ? error.message : String(error);
773
+ this.logger.error(`Consumer onSet (immediate) failed for ${registration.capability}: ${msg}`);
704
774
  }
705
775
  }
706
- /** List installed addons (directories with package.json containing camstack.addons) */
707
- listInstalled() {
708
- if (!fs.existsSync(this.addonsDir))
709
- return [];
710
- return fs.readdirSync(this.addonsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
711
- const pkgPath = path.join(this.addonsDir, d.name, "package.json");
712
- if (!fs.existsSync(pkgPath))
713
- return null;
776
+ } else {
777
+ if (registration.onAdded) {
778
+ for (const entry of state.activeCollection) {
714
779
  try {
715
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
716
- if (!pkg.camstack?.addons)
717
- return null;
718
- return { name: pkg.name, version: pkg.version, dir: path.join(this.addonsDir, d.name) };
719
- } catch {
720
- return null;
780
+ registration.onAdded(entry.provider);
781
+ } catch (error) {
782
+ const msg = error instanceof Error ? error.message : String(error);
783
+ this.logger.error(`Consumer onAdded (immediate) failed for ${registration.capability}: ${msg}`);
721
784
  }
722
- }).filter((item) => item !== null);
723
- }
724
- /** Check if an addon is installed */
725
- isInstalled(packageName) {
726
- if (fs.existsSync(path.join(this.addonsDir, packageName, "package.json")))
727
- return true;
728
- const legacy = packageName.replace(/^@[^/]+\//, "");
729
- return fs.existsSync(path.join(this.addonsDir, legacy, "package.json"));
730
- }
731
- /** Get installed package info */
732
- getInstalledPackage(packageName) {
733
- let pkgPath = path.join(this.addonsDir, packageName, "package.json");
734
- if (!fs.existsSync(pkgPath)) {
735
- pkgPath = path.join(this.addonsDir, packageName.replace(/^@[^/]+\//, ""), "package.json");
736
- }
737
- if (!fs.existsSync(pkgPath))
738
- return null;
739
- try {
740
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
741
- return { name: pkg.name, version: pkg.version, dir: path.dirname(pkgPath) };
742
- } catch {
743
- return null;
744
785
  }
745
786
  }
787
+ }
788
+ return () => {
789
+ state.consumers.delete(untypedReg);
746
790
  };
747
- exports2.AddonInstaller = AddonInstaller2;
748
791
  }
749
- });
750
-
751
- // src/workspace-detect.js
752
- var require_workspace_detect = __commonJS({
753
- "src/workspace-detect.js"(exports2) {
754
- "use strict";
755
- var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
756
- if (k2 === void 0) k2 = k;
757
- var desc = Object.getOwnPropertyDescriptor(m, k);
758
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
759
- desc = { enumerable: true, get: function() {
760
- return m[k];
761
- } };
762
- }
763
- Object.defineProperty(o, k2, desc);
764
- }) : (function(o, m, k, k2) {
765
- if (k2 === void 0) k2 = k;
766
- o[k2] = m[k];
767
- }));
768
- var __setModuleDefault = exports2 && exports2.__setModuleDefault || (Object.create ? (function(o, v) {
769
- Object.defineProperty(o, "default", { enumerable: true, value: v });
770
- }) : function(o, v) {
771
- o["default"] = v;
792
+ /**
793
+ * Get the active singleton provider for a capability.
794
+ * Returns null if none set.
795
+ */
796
+ getSingleton(capability) {
797
+ const state = this.capabilities.get(capability);
798
+ if (!state || state.declaration.mode !== "singleton") return null;
799
+ return state.activeProvider ?? null;
800
+ }
801
+ /**
802
+ * Get all active collection providers for a capability.
803
+ */
804
+ getCollection(capability) {
805
+ const state = this.capabilities.get(capability);
806
+ if (!state || state.declaration.mode !== "collection") return [];
807
+ return state.activeCollection.map((e) => e.provider);
808
+ }
809
+ /**
810
+ * Set which addon should be the active singleton for a capability.
811
+ * Call with `immediate: true` to also swap the runtime provider now
812
+ * (consumers' onSet will be awaited).
813
+ */
814
+ async setActiveSingleton(capability, addonId, immediate = false) {
815
+ const state = this.capabilities.get(capability);
816
+ if (!state) {
817
+ throw new Error(`Unknown capability: ${capability}`);
818
+ }
819
+ if (state.declaration.mode !== "singleton") {
820
+ throw new Error(`Capability "${capability}" is not a singleton`);
821
+ }
822
+ const provider = state.available.get(addonId);
823
+ if (!provider) {
824
+ throw new Error(`No provider "${addonId}" registered for capability "${capability}"`);
825
+ }
826
+ if (immediate) {
827
+ await this.activateSingletonAsync(state, addonId, provider);
828
+ }
829
+ this.logger.info(`Singleton preference set: ${capability} \u2192 ${addonId}`);
830
+ }
831
+ /**
832
+ * Get the mode declared for a capability.
833
+ */
834
+ getMode(capability) {
835
+ return this.capabilities.get(capability)?.declaration.mode;
836
+ }
837
+ /**
838
+ * List all registered capabilities with their providers.
839
+ */
840
+ listCapabilities() {
841
+ const result = [];
842
+ for (const [name, state] of this.capabilities) {
843
+ result.push({
844
+ name,
845
+ mode: state.declaration.mode,
846
+ providers: [...state.available.keys()],
847
+ activeProvider: state.activeAddonId
848
+ });
849
+ }
850
+ return result;
851
+ }
852
+ /**
853
+ * Check if all dependencies for a capability are satisfied (have active providers).
854
+ */
855
+ areDependenciesMet(declaration) {
856
+ if (!declaration.dependsOn?.length) return true;
857
+ return declaration.dependsOn.every((dep) => {
858
+ const state = this.capabilities.get(dep);
859
+ if (!state) return false;
860
+ if (state.declaration.mode === "singleton") {
861
+ return state.activeProvider !== null;
862
+ }
863
+ return state.activeCollection.length > 0;
772
864
  });
773
- var __importStar = exports2 && exports2.__importStar || /* @__PURE__ */ (function() {
774
- var ownKeys = function(o) {
775
- ownKeys = Object.getOwnPropertyNames || function(o2) {
776
- var ar = [];
777
- for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
778
- return ar;
779
- };
780
- return ownKeys(o);
781
- };
782
- return function(mod) {
783
- if (mod && mod.__esModule) return mod;
784
- var result = {};
785
- if (mod != null) {
786
- for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
787
- }
788
- __setModuleDefault(result, mod);
789
- return result;
790
- };
791
- })();
792
- Object.defineProperty(exports2, "__esModule", { value: true });
793
- exports2.detectWorkspacePackagesDir = detectWorkspacePackagesDir2;
794
- var fs = __importStar(require("fs"));
795
- var path = __importStar(require("path"));
796
- function detectWorkspacePackagesDir2(startDir) {
797
- let current = path.resolve(startDir);
798
- for (let i = 0; i < 6; i++) {
799
- current = path.dirname(current);
800
- const packagesDir = path.join(current, "packages");
801
- const rootPkgJson = path.join(current, "package.json");
802
- if (fs.existsSync(packagesDir) && fs.existsSync(rootPkgJson)) {
803
- try {
804
- const rootPkg = JSON.parse(fs.readFileSync(rootPkgJson, "utf-8"));
805
- if (rootPkg.workspaces || rootPkg.name === "camstack-server" || rootPkg.name === "camstack") {
806
- return packagesDir;
807
- }
808
- } catch {
809
- }
810
- }
865
+ }
866
+ /**
867
+ * Get the dependency-ordered list of capability names for boot sequencing.
868
+ * Returns capabilities sorted topologically by dependsOn.
869
+ * Throws if a cycle is detected.
870
+ */
871
+ getBootOrder() {
872
+ const visited = /* @__PURE__ */ new Set();
873
+ const visiting = /* @__PURE__ */ new Set();
874
+ const order = [];
875
+ const visit = (name) => {
876
+ if (visited.has(name)) return;
877
+ if (visiting.has(name)) {
878
+ throw new Error(`Circular dependency detected involving capability "${name}"`);
879
+ }
880
+ visiting.add(name);
881
+ const state = this.capabilities.get(name);
882
+ if (state?.declaration.dependsOn) {
883
+ for (const dep of state.declaration.dependsOn) {
884
+ visit(dep);
885
+ }
886
+ }
887
+ visiting.delete(name);
888
+ visited.add(name);
889
+ order.push(name);
890
+ };
891
+ for (const name of this.capabilities.keys()) {
892
+ visit(name);
893
+ }
894
+ return order;
895
+ }
896
+ // ---- Per-device overrides ----
897
+ /**
898
+ * Set a per-device singleton override. When resolveForDevice is called for
899
+ * this device + capability, the specified addon's provider is returned
900
+ * instead of the global singleton.
901
+ */
902
+ setDeviceOverride(deviceId, capability, addonId) {
903
+ const state = this.capabilities.get(capability);
904
+ if (!state) {
905
+ this.logger.warn(`Cannot set device override for undeclared capability "${capability}"`);
906
+ return;
907
+ }
908
+ if (!state.available.has(addonId)) {
909
+ this.logger.warn(`Cannot set device override: addon "${addonId}" not registered for "${capability}"`);
910
+ return;
911
+ }
912
+ let deviceMap = this.deviceOverrides.get(deviceId);
913
+ if (!deviceMap) {
914
+ deviceMap = /* @__PURE__ */ new Map();
915
+ this.deviceOverrides.set(deviceId, deviceMap);
916
+ }
917
+ deviceMap.set(capability, addonId);
918
+ this.logger.info(`Device override set: ${deviceId} \u2192 ${capability} = ${addonId}`);
919
+ }
920
+ /**
921
+ * Clear a per-device singleton override, reverting to the global singleton.
922
+ */
923
+ clearDeviceOverride(deviceId, capability) {
924
+ const deviceMap = this.deviceOverrides.get(deviceId);
925
+ if (!deviceMap) return;
926
+ deviceMap.delete(capability);
927
+ if (deviceMap.size === 0) {
928
+ this.deviceOverrides.delete(deviceId);
929
+ }
930
+ this.logger.info(`Device override cleared: ${deviceId} \u2192 ${capability}`);
931
+ }
932
+ /**
933
+ * Get all per-device singleton overrides for a device.
934
+ * Returns a Map of capability name to addon ID.
935
+ */
936
+ getDeviceOverrides(deviceId) {
937
+ return new Map(this.deviceOverrides.get(deviceId) ?? []);
938
+ }
939
+ /**
940
+ * Resolve a singleton provider for a specific device.
941
+ * 1. Check device override — return that addon's provider
942
+ * 2. Fallback to global singleton
943
+ */
944
+ resolveForDevice(capability, deviceId) {
945
+ const state = this.capabilities.get(capability);
946
+ if (!state || state.declaration.mode !== "singleton") return null;
947
+ const deviceMap = this.deviceOverrides.get(deviceId);
948
+ if (deviceMap) {
949
+ const overrideAddonId = deviceMap.get(capability);
950
+ if (overrideAddonId) {
951
+ const provider = state.available.get(overrideAddonId);
952
+ if (provider) return provider;
953
+ this.logger.warn(
954
+ `Device override for ${deviceId}/${capability} references unregistered addon "${overrideAddonId}" \u2014 falling back to global`
955
+ );
811
956
  }
812
- return null;
813
957
  }
958
+ return state.activeProvider ?? null;
814
959
  }
815
- });
816
-
817
- // src/capability-registry.js
818
- var require_capability_registry = __commonJS({
819
- "src/capability-registry.js"(exports2) {
820
- "use strict";
821
- Object.defineProperty(exports2, "__esModule", { value: true });
822
- exports2.CapabilityRegistry = void 0;
823
- var CapabilityRegistry2 = class {
824
- logger;
825
- configReader;
826
- capabilities = /* @__PURE__ */ new Map();
827
- /** Per-device singleton overrides: deviceId → (capability → addonId) */
828
- deviceOverrides = /* @__PURE__ */ new Map();
829
- /** Per-device collection filters: deviceId → (capability → addonIds[]) */
830
- deviceCollectionFilters = /* @__PURE__ */ new Map();
831
- constructor(logger, configReader) {
832
- this.logger = logger;
833
- this.configReader = configReader;
960
+ /**
961
+ * Set a per-device collection filter. When resolveCollectionForDevice is called
962
+ * for this device + capability, only providers from the specified addon IDs
963
+ * are returned instead of the full collection.
964
+ */
965
+ setDeviceCollectionFilter(deviceId, capability, addonIds) {
966
+ const state = this.capabilities.get(capability);
967
+ if (!state) {
968
+ this.logger.warn(`Cannot set device collection filter for undeclared capability "${capability}"`);
969
+ return;
970
+ }
971
+ let deviceMap = this.deviceCollectionFilters.get(deviceId);
972
+ if (!deviceMap) {
973
+ deviceMap = /* @__PURE__ */ new Map();
974
+ this.deviceCollectionFilters.set(deviceId, deviceMap);
975
+ }
976
+ deviceMap.set(capability, [...addonIds]);
977
+ this.logger.info(`Device collection filter set: ${deviceId} \u2192 ${capability} = [${addonIds.join(", ")}]`);
978
+ }
979
+ /**
980
+ * Clear a per-device collection filter, reverting to the full collection.
981
+ */
982
+ clearDeviceCollectionFilter(deviceId, capability) {
983
+ const deviceMap = this.deviceCollectionFilters.get(deviceId);
984
+ if (!deviceMap) return;
985
+ deviceMap.delete(capability);
986
+ if (deviceMap.size === 0) {
987
+ this.deviceCollectionFilters.delete(deviceId);
988
+ }
989
+ this.logger.info(`Device collection filter cleared: ${deviceId} \u2192 ${capability}`);
990
+ }
991
+ /**
992
+ * Resolve collection providers for a specific device.
993
+ * If a filter exists for the device + capability, only those addon's providers are returned.
994
+ * If no filter exists, the full collection is returned.
995
+ */
996
+ resolveCollectionForDevice(capability, deviceId) {
997
+ const state = this.capabilities.get(capability);
998
+ if (!state || state.declaration.mode !== "collection") return [];
999
+ const deviceMap = this.deviceCollectionFilters.get(deviceId);
1000
+ if (deviceMap) {
1001
+ const filterAddonIds = deviceMap.get(capability);
1002
+ if (filterAddonIds) {
1003
+ const filterSet = new Set(filterAddonIds);
1004
+ return state.activeCollection.filter((e) => filterSet.has(e.addonId)).map((e) => e.provider);
834
1005
  }
835
- /**
836
- * Declare a capability (typically called when addon manifests are loaded).
837
- * Must be called before registerProvider/registerConsumer for that capability.
838
- */
839
- declareCapability(declaration) {
840
- if (this.capabilities.has(declaration.name)) {
841
- this.logger.debug(`Capability "${declaration.name}" already declared, skipping`);
842
- return;
1006
+ }
1007
+ return state.activeCollection.map((e) => e.provider);
1008
+ }
1009
+ /**
1010
+ * Get a specific addon's provider by addon ID, regardless of whether it's the active singleton.
1011
+ * Useful for per-device overrides that need to look up any registered provider.
1012
+ */
1013
+ getProviderByAddonId(capability, addonId) {
1014
+ const state = this.capabilities.get(capability);
1015
+ if (!state) return null;
1016
+ const provider = state.available.get(addonId);
1017
+ return provider ?? null;
1018
+ }
1019
+ activateSingleton(state, addonId, provider) {
1020
+ state.activeAddonId = addonId;
1021
+ state.activeProvider = provider;
1022
+ this.logger.info(`Singleton activated: ${state.declaration.name} \u2192 ${addonId}`);
1023
+ for (const consumer of state.consumers) {
1024
+ if (consumer.onSet) {
1025
+ try {
1026
+ consumer.onSet(provider);
1027
+ } catch (error) {
1028
+ const msg = error instanceof Error ? error.message : String(error);
1029
+ this.logger.error(`Consumer onSet failed for ${state.declaration.name}: ${msg}`);
843
1030
  }
844
- this.capabilities.set(declaration.name, {
845
- declaration,
846
- available: /* @__PURE__ */ new Map(),
847
- activeAddonId: null,
848
- activeProvider: null,
849
- activeCollection: [],
850
- consumers: /* @__PURE__ */ new Set()
851
- });
852
- this.logger.debug(`Capability declared: ${declaration.name} (mode=${declaration.mode})`);
853
1031
  }
854
- /**
855
- * Register a capability provider (called by addon loader when addon is enabled).
856
- * For singleton: auto-activates if user-preferred or first registered.
857
- * For collection: adds to active set and notifies consumers.
858
- */
859
- registerProvider(capability, addonId, provider) {
860
- const state = this.capabilities.get(capability);
861
- if (!state) {
862
- this.logger.warn(`Cannot register provider for undeclared capability "${capability}"`);
863
- return;
1032
+ }
1033
+ }
1034
+ async activateSingletonAsync(state, addonId, provider) {
1035
+ state.activeAddonId = addonId;
1036
+ state.activeProvider = provider;
1037
+ this.logger.info(`Singleton activated (async): ${state.declaration.name} \u2192 ${addonId}`);
1038
+ for (const consumer of state.consumers) {
1039
+ if (consumer.onSet) {
1040
+ try {
1041
+ await consumer.onSet(provider);
1042
+ } catch (error) {
1043
+ const msg = error instanceof Error ? error.message : String(error);
1044
+ this.logger.error(`Consumer onSet (async) failed for ${state.declaration.name}: ${msg}`);
864
1045
  }
865
- state.available.set(addonId, provider);
866
- this.logger.info(`Provider registered: ${addonId} \u2192 ${capability}`);
867
- if (state.declaration.mode === "singleton") {
868
- const userChoice = this.configReader(capability);
869
- if (userChoice === addonId) {
870
- this.activateSingleton(state, addonId, provider);
871
- } else if (userChoice === void 0 && state.activeAddonId === null) {
872
- this.activateSingleton(state, addonId, provider);
873
- }
874
- } else {
875
- state.activeCollection.push({ addonId, provider });
876
- for (const consumer of state.consumers) {
877
- if (consumer.onAdded) {
878
- try {
879
- consumer.onAdded(provider);
880
- } catch (error) {
881
- const msg = error instanceof Error ? error.message : String(error);
882
- this.logger.error(`Consumer onAdded failed for ${capability}: ${msg}`);
883
- }
884
- }
885
- }
886
- }
887
- }
888
- /**
889
- * Unregister a provider (called when addon is disabled/uninstalled).
890
- */
891
- unregisterProvider(capability, addonId) {
892
- const state = this.capabilities.get(capability);
893
- if (!state)
894
- return;
895
- const provider = state.available.get(addonId);
896
- state.available.delete(addonId);
897
- if (state.declaration.mode === "singleton") {
898
- if (state.activeAddonId === addonId) {
899
- state.activeAddonId = null;
900
- state.activeProvider = null;
901
- this.logger.info(`Singleton deactivated: ${capability} (was ${addonId})`);
902
- }
903
- } else {
904
- const idx = state.activeCollection.findIndex((e) => e.addonId === addonId);
905
- if (idx !== -1) {
906
- state.activeCollection.splice(idx, 1);
907
- for (const consumer of state.consumers) {
908
- if (consumer.onRemoved && provider !== void 0) {
909
- try {
910
- consumer.onRemoved(provider);
911
- } catch (error) {
912
- const msg = error instanceof Error ? error.message : String(error);
913
- this.logger.error(`Consumer onRemoved failed for ${capability}: ${msg}`);
914
- }
915
- }
916
- }
917
- }
918
- }
919
- }
920
- /**
921
- * Register a consumer that wants to be notified when providers change.
922
- * If a provider is already active, the consumer is immediately notified.
923
- * Returns a disposer function for cleanup.
924
- */
925
- registerConsumer(registration) {
926
- const state = this.capabilities.get(registration.capability);
927
- if (!state) {
928
- this.logger.debug(`Consumer registered for undeclared capability "${registration.capability}" \u2014 auto-declaring`);
929
- this.declareCapability({ name: registration.capability, mode: "singleton" });
930
- return this.registerConsumer(registration);
931
- }
932
- const untypedReg = registration;
933
- state.consumers.add(untypedReg);
934
- if (state.declaration.mode === "singleton") {
935
- if (state.activeProvider !== null && registration.onSet) {
936
- try {
937
- registration.onSet(state.activeProvider);
938
- } catch (error) {
939
- const msg = error instanceof Error ? error.message : String(error);
940
- this.logger.error(`Consumer onSet (immediate) failed for ${registration.capability}: ${msg}`);
941
- }
942
- }
943
- } else {
944
- if (registration.onAdded) {
945
- for (const entry of state.activeCollection) {
946
- try {
947
- registration.onAdded(entry.provider);
948
- } catch (error) {
949
- const msg = error instanceof Error ? error.message : String(error);
950
- this.logger.error(`Consumer onAdded (immediate) failed for ${registration.capability}: ${msg}`);
951
- }
952
- }
953
- }
954
- }
955
- return () => {
956
- state.consumers.delete(untypedReg);
957
- };
958
- }
959
- /**
960
- * Get the active singleton provider for a capability.
961
- * Returns null if none set.
962
- */
963
- getSingleton(capability) {
964
- const state = this.capabilities.get(capability);
965
- if (!state || state.declaration.mode !== "singleton")
966
- return null;
967
- return state.activeProvider ?? null;
968
- }
969
- /**
970
- * Get all active collection providers for a capability.
971
- */
972
- getCollection(capability) {
973
- const state = this.capabilities.get(capability);
974
- if (!state || state.declaration.mode !== "collection")
975
- return [];
976
- return state.activeCollection.map((e) => e.provider);
977
- }
978
- /**
979
- * Set which addon should be the active singleton for a capability.
980
- * Call with `immediate: true` to also swap the runtime provider now
981
- * (consumers' onSet will be awaited).
982
- */
983
- async setActiveSingleton(capability, addonId, immediate = false) {
984
- const state = this.capabilities.get(capability);
985
- if (!state) {
986
- throw new Error(`Unknown capability: ${capability}`);
987
- }
988
- if (state.declaration.mode !== "singleton") {
989
- throw new Error(`Capability "${capability}" is not a singleton`);
990
- }
991
- const provider = state.available.get(addonId);
992
- if (!provider) {
993
- throw new Error(`No provider "${addonId}" registered for capability "${capability}"`);
994
- }
995
- if (immediate) {
996
- await this.activateSingletonAsync(state, addonId, provider);
997
- }
998
- this.logger.info(`Singleton preference set: ${capability} \u2192 ${addonId}`);
999
- }
1000
- /**
1001
- * Get the mode declared for a capability.
1002
- */
1003
- getMode(capability) {
1004
- return this.capabilities.get(capability)?.declaration.mode;
1005
- }
1006
- /**
1007
- * List all registered capabilities with their providers.
1008
- */
1009
- listCapabilities() {
1010
- const result = [];
1011
- for (const [name, state] of this.capabilities) {
1012
- result.push({
1013
- name,
1014
- mode: state.declaration.mode,
1015
- providers: [...state.available.keys()],
1016
- activeProvider: state.activeAddonId
1017
- });
1018
- }
1019
- return result;
1020
- }
1021
- /**
1022
- * Check if all dependencies for a capability are satisfied (have active providers).
1023
- */
1024
- areDependenciesMet(declaration) {
1025
- if (!declaration.dependsOn?.length)
1026
- return true;
1027
- return declaration.dependsOn.every((dep) => {
1028
- const state = this.capabilities.get(dep);
1029
- if (!state)
1030
- return false;
1031
- if (state.declaration.mode === "singleton") {
1032
- return state.activeProvider !== null;
1033
- }
1034
- return state.activeCollection.length > 0;
1035
- });
1036
- }
1037
- /**
1038
- * Get the dependency-ordered list of capability names for boot sequencing.
1039
- * Returns capabilities sorted topologically by dependsOn.
1040
- * Throws if a cycle is detected.
1041
- */
1042
- getBootOrder() {
1043
- const visited = /* @__PURE__ */ new Set();
1044
- const visiting = /* @__PURE__ */ new Set();
1045
- const order = [];
1046
- const visit = (name) => {
1047
- if (visited.has(name))
1048
- return;
1049
- if (visiting.has(name)) {
1050
- throw new Error(`Circular dependency detected involving capability "${name}"`);
1051
- }
1052
- visiting.add(name);
1053
- const state = this.capabilities.get(name);
1054
- if (state?.declaration.dependsOn) {
1055
- for (const dep of state.declaration.dependsOn) {
1056
- visit(dep);
1057
- }
1058
- }
1059
- visiting.delete(name);
1060
- visited.add(name);
1061
- order.push(name);
1062
- };
1063
- for (const name of this.capabilities.keys()) {
1064
- visit(name);
1065
- }
1066
- return order;
1067
- }
1068
- // ---- Per-device overrides ----
1069
- /**
1070
- * Set a per-device singleton override. When resolveForDevice is called for
1071
- * this device + capability, the specified addon's provider is returned
1072
- * instead of the global singleton.
1073
- */
1074
- setDeviceOverride(deviceId, capability, addonId) {
1075
- const state = this.capabilities.get(capability);
1076
- if (!state) {
1077
- this.logger.warn(`Cannot set device override for undeclared capability "${capability}"`);
1078
- return;
1079
- }
1080
- if (!state.available.has(addonId)) {
1081
- this.logger.warn(`Cannot set device override: addon "${addonId}" not registered for "${capability}"`);
1082
- return;
1083
- }
1084
- let deviceMap = this.deviceOverrides.get(deviceId);
1085
- if (!deviceMap) {
1086
- deviceMap = /* @__PURE__ */ new Map();
1087
- this.deviceOverrides.set(deviceId, deviceMap);
1088
- }
1089
- deviceMap.set(capability, addonId);
1090
- this.logger.info(`Device override set: ${deviceId} \u2192 ${capability} = ${addonId}`);
1091
- }
1092
- /**
1093
- * Clear a per-device singleton override, reverting to the global singleton.
1094
- */
1095
- clearDeviceOverride(deviceId, capability) {
1096
- const deviceMap = this.deviceOverrides.get(deviceId);
1097
- if (!deviceMap)
1098
- return;
1099
- deviceMap.delete(capability);
1100
- if (deviceMap.size === 0) {
1101
- this.deviceOverrides.delete(deviceId);
1102
- }
1103
- this.logger.info(`Device override cleared: ${deviceId} \u2192 ${capability}`);
1104
1046
  }
1105
- /**
1106
- * Get all per-device singleton overrides for a device.
1107
- * Returns a Map of capability name to addon ID.
1108
- */
1109
- getDeviceOverrides(deviceId) {
1110
- return new Map(this.deviceOverrides.get(deviceId) ?? []);
1111
- }
1112
- /**
1113
- * Resolve a singleton provider for a specific device.
1114
- * 1. Check device override — return that addon's provider
1115
- * 2. Fallback to global singleton
1116
- */
1117
- resolveForDevice(capability, deviceId) {
1118
- const state = this.capabilities.get(capability);
1119
- if (!state || state.declaration.mode !== "singleton")
1120
- return null;
1121
- const deviceMap = this.deviceOverrides.get(deviceId);
1122
- if (deviceMap) {
1123
- const overrideAddonId = deviceMap.get(capability);
1124
- if (overrideAddonId) {
1125
- const provider = state.available.get(overrideAddonId);
1126
- if (provider)
1127
- return provider;
1128
- this.logger.warn(`Device override for ${deviceId}/${capability} references unregistered addon "${overrideAddonId}" \u2014 falling back to global`);
1129
- }
1130
- }
1131
- return state.activeProvider ?? null;
1132
- }
1133
- /**
1134
- * Set a per-device collection filter. When resolveCollectionForDevice is called
1135
- * for this device + capability, only providers from the specified addon IDs
1136
- * are returned instead of the full collection.
1137
- */
1138
- setDeviceCollectionFilter(deviceId, capability, addonIds) {
1139
- const state = this.capabilities.get(capability);
1140
- if (!state) {
1141
- this.logger.warn(`Cannot set device collection filter for undeclared capability "${capability}"`);
1142
- return;
1143
- }
1144
- let deviceMap = this.deviceCollectionFilters.get(deviceId);
1145
- if (!deviceMap) {
1146
- deviceMap = /* @__PURE__ */ new Map();
1147
- this.deviceCollectionFilters.set(deviceId, deviceMap);
1148
- }
1149
- deviceMap.set(capability, [...addonIds]);
1150
- this.logger.info(`Device collection filter set: ${deviceId} \u2192 ${capability} = [${addonIds.join(", ")}]`);
1151
- }
1152
- /**
1153
- * Clear a per-device collection filter, reverting to the full collection.
1154
- */
1155
- clearDeviceCollectionFilter(deviceId, capability) {
1156
- const deviceMap = this.deviceCollectionFilters.get(deviceId);
1157
- if (!deviceMap)
1158
- return;
1159
- deviceMap.delete(capability);
1160
- if (deviceMap.size === 0) {
1161
- this.deviceCollectionFilters.delete(deviceId);
1162
- }
1163
- this.logger.info(`Device collection filter cleared: ${deviceId} \u2192 ${capability}`);
1164
- }
1165
- /**
1166
- * Resolve collection providers for a specific device.
1167
- * If a filter exists for the device + capability, only those addon's providers are returned.
1168
- * If no filter exists, the full collection is returned.
1169
- */
1170
- resolveCollectionForDevice(capability, deviceId) {
1171
- const state = this.capabilities.get(capability);
1172
- if (!state || state.declaration.mode !== "collection")
1173
- return [];
1174
- const deviceMap = this.deviceCollectionFilters.get(deviceId);
1175
- if (deviceMap) {
1176
- const filterAddonIds = deviceMap.get(capability);
1177
- if (filterAddonIds) {
1178
- const filterSet = new Set(filterAddonIds);
1179
- return state.activeCollection.filter((e) => filterSet.has(e.addonId)).map((e) => e.provider);
1180
- }
1181
- }
1182
- return state.activeCollection.map((e) => e.provider);
1183
- }
1184
- /**
1185
- * Get a specific addon's provider by addon ID, regardless of whether it's the active singleton.
1186
- * Useful for per-device overrides that need to look up any registered provider.
1187
- */
1188
- getProviderByAddonId(capability, addonId) {
1189
- const state = this.capabilities.get(capability);
1190
- if (!state)
1191
- return null;
1192
- const provider = state.available.get(addonId);
1193
- return provider ?? null;
1194
- }
1195
- activateSingleton(state, addonId, provider) {
1196
- state.activeAddonId = addonId;
1197
- state.activeProvider = provider;
1198
- this.logger.info(`Singleton activated: ${state.declaration.name} \u2192 ${addonId}`);
1199
- for (const consumer of state.consumers) {
1200
- if (consumer.onSet) {
1201
- try {
1202
- consumer.onSet(provider);
1203
- } catch (error) {
1204
- const msg = error instanceof Error ? error.message : String(error);
1205
- this.logger.error(`Consumer onSet failed for ${state.declaration.name}: ${msg}`);
1206
- }
1207
- }
1208
- }
1209
- }
1210
- async activateSingletonAsync(state, addonId, provider) {
1211
- state.activeAddonId = addonId;
1212
- state.activeProvider = provider;
1213
- this.logger.info(`Singleton activated (async): ${state.declaration.name} \u2192 ${addonId}`);
1214
- for (const consumer of state.consumers) {
1215
- if (consumer.onSet) {
1216
- try {
1217
- await consumer.onSet(provider);
1218
- } catch (error) {
1219
- const msg = error instanceof Error ? error.message : String(error);
1220
- this.logger.error(`Consumer onSet (async) failed for ${state.declaration.name}: ${msg}`);
1221
- }
1222
- }
1223
- }
1224
- }
1225
- };
1226
- exports2.CapabilityRegistry = CapabilityRegistry2;
1047
+ }
1227
1048
  }
1049
+ };
1050
+
1051
+ // src/infra-capabilities.ts
1052
+ var INFRA_CAPABILITIES = [
1053
+ { name: "storage", required: true },
1054
+ { name: "settings-store", required: true },
1055
+ { name: "log-destination", required: false }
1056
+ ];
1057
+ var infraNames = new Set(INFRA_CAPABILITIES.map((c) => c.name));
1058
+ function isInfraCapability(name) {
1059
+ return infraNames.has(name);
1060
+ }
1061
+
1062
+ // src/config-manager.ts
1063
+ var fs5 = __toESM(require("fs"));
1064
+ var yaml = __toESM(require("js-yaml"));
1065
+
1066
+ // src/config-schema.ts
1067
+ var import_zod = require("zod");
1068
+ var DEFAULT_DATA_PATH = "camstack-data";
1069
+ var bootstrapSchema = import_zod.z.object({
1070
+ /** Server mode: 'hub' (full server) or 'agent' (worker node) */
1071
+ mode: import_zod.z.enum(["hub", "agent"]).default("hub"),
1072
+ server: import_zod.z.object({
1073
+ port: import_zod.z.number().default(4443),
1074
+ host: import_zod.z.string().default("0.0.0.0"),
1075
+ dataPath: import_zod.z.string().default(DEFAULT_DATA_PATH)
1076
+ }).default({}),
1077
+ auth: import_zod.z.object({
1078
+ jwtSecret: import_zod.z.string().nullable().default(null),
1079
+ adminUsername: import_zod.z.string().default("admin"),
1080
+ adminPassword: import_zod.z.string().default(process.env.ADMIN_PASSWORD ?? "changeme")
1081
+ }).default({}),
1082
+ /** Hub connection config — only used when mode='agent' */
1083
+ hub: import_zod.z.object({
1084
+ url: import_zod.z.string().default("ws://localhost:4443/agent"),
1085
+ token: import_zod.z.string().default("")
1086
+ }).default({}),
1087
+ /** Agent-specific config — only used when mode='agent' */
1088
+ agent: import_zod.z.object({
1089
+ name: import_zod.z.string().default(""),
1090
+ /** Port for the agent status page (minimal HTML) */
1091
+ statusPort: import_zod.z.number().default(4444)
1092
+ }).default({}),
1093
+ /** TLS configuration */
1094
+ tls: import_zod.z.object({
1095
+ /** Enable HTTPS (default: true) */
1096
+ enabled: import_zod.z.boolean().default(true),
1097
+ /** Path to custom cert file (PEM). If not set, auto-generates self-signed. */
1098
+ certPath: import_zod.z.string().optional(),
1099
+ /** Path to custom key file (PEM). Required if certPath is set. */
1100
+ keyPath: import_zod.z.string().optional()
1101
+ }).default({})
1228
1102
  });
1103
+ var RUNTIME_DEFAULTS = {
1104
+ "features.streaming": true,
1105
+ "features.notifications": true,
1106
+ "features.objectDetection": false,
1107
+ "features.remoteAccess": true,
1108
+ "features.agentCluster": false,
1109
+ "features.smartHome": true,
1110
+ "features.recordings": true,
1111
+ "features.backup": true,
1112
+ "features.repl": true,
1113
+ "retention.detectionEventsDays": 30,
1114
+ "retention.audioLevelsDays": 7,
1115
+ "logging.level": "info",
1116
+ "logging.retentionDays": 30,
1117
+ "eventBus.ringBufferSize": 1e4,
1118
+ "storage.provider": "sqlite-storage",
1119
+ "storage.locations": {
1120
+ data: "camstack-data/data",
1121
+ media: "camstack-data/media",
1122
+ recordings: "camstack-data/recordings",
1123
+ cache: "/tmp/camstack-cache",
1124
+ logs: "camstack-data/logs",
1125
+ models: "camstack-data/models"
1126
+ },
1127
+ "providers": [],
1128
+ // Recording
1129
+ "recording.segmentDurationSeconds": 4,
1130
+ "recording.defaultRetentionDays": 30,
1131
+ // Streaming ports are addon-specific (go2rtc owns its defaults)
1132
+ // FFmpeg
1133
+ "ffmpeg.binaryPath": "ffmpeg",
1134
+ "ffmpeg.hwAccel": "auto",
1135
+ "ffmpeg.threadCount": 0,
1136
+ // Detection defaults
1137
+ "detection.defaultMotionFps": 2,
1138
+ "detection.defaultDetectionFps": 5,
1139
+ "detection.defaultCooldownSeconds": 10,
1140
+ "detection.defaultConfidenceThreshold": 0.4,
1141
+ "detection.trackerMaxAgeFrames": 30,
1142
+ "detection.trackerMinHits": 3,
1143
+ "detection.trackerIouThreshold": 0.3,
1144
+ // Backup retention is addon-specific (local-backup owns its default)
1145
+ // Auth (runtime)
1146
+ "auth.tokenExpiry": "24h"
1147
+ };
1229
1148
 
1230
- // src/infra-capabilities.js
1231
- var require_infra_capabilities = __commonJS({
1232
- "src/infra-capabilities.js"(exports2) {
1233
- "use strict";
1234
- Object.defineProperty(exports2, "__esModule", { value: true });
1235
- exports2.INFRA_CAPABILITIES = void 0;
1236
- exports2.isInfraCapability = isInfraCapability2;
1237
- exports2.INFRA_CAPABILITIES = [
1238
- { name: "storage", required: true },
1239
- { name: "settings-store", required: true },
1240
- { name: "log-destination", required: false }
1241
- ];
1242
- var infraNames = new Set(exports2.INFRA_CAPABILITIES.map((c) => c.name));
1243
- function isInfraCapability2(name) {
1244
- return infraNames.has(name);
1149
+ // src/config-manager.ts
1150
+ var ENV_VAR_MAP = {
1151
+ CAMSTACK_PORT: "server.port",
1152
+ CAMSTACK_HOST: "server.host",
1153
+ CAMSTACK_DATA: "server.dataPath",
1154
+ CAMSTACK_JWT_SECRET: "auth.jwtSecret",
1155
+ CAMSTACK_ADMIN_USER: "auth.adminUsername",
1156
+ CAMSTACK_ADMIN_PASS: "auth.adminPassword"
1157
+ };
1158
+ var ConfigManager = class _ConfigManager {
1159
+ constructor(configPath) {
1160
+ this.configPath = configPath;
1161
+ const rawYaml = this.loadYaml();
1162
+ const merged = this.applyEnvOverrides(rawYaml);
1163
+ this.bootstrapConfig = bootstrapSchema.parse(merged);
1164
+ this.warnDefaultCredentials();
1165
+ }
1166
+ // Non-readonly so update() can sync the in-memory view after a write.
1167
+ bootstrapConfig;
1168
+ settingsStore = null;
1169
+ /** Called by main.ts after the SQLite DB is ready (Phase 2). */
1170
+ setSettingsStore(store) {
1171
+ this.settingsStore = store;
1172
+ }
1173
+ /**
1174
+ * Get a config value by dot-notation path.
1175
+ * Priority: bootstrap config -> SQL system_settings -> RUNTIME_DEFAULTS fallback.
1176
+ */
1177
+ get(path5) {
1178
+ const bootstrapValue = this.getFromBootstrap(path5);
1179
+ if (bootstrapValue !== void 0) {
1180
+ return bootstrapValue;
1181
+ }
1182
+ if (this.settingsStore !== null) {
1183
+ const sqlValue = this.settingsStore.getSystem(path5);
1184
+ if (sqlValue !== void 0) {
1185
+ return sqlValue;
1186
+ }
1187
+ const sqlNested = this.getNestedFromSystemSettings(path5);
1188
+ if (sqlNested !== void 0) {
1189
+ return sqlNested;
1190
+ }
1191
+ }
1192
+ if (path5 in RUNTIME_DEFAULTS) {
1193
+ return RUNTIME_DEFAULTS[path5];
1194
+ }
1195
+ const nested = this.getFromRuntimeDefaults(path5);
1196
+ if (nested !== void 0) {
1197
+ return nested;
1245
1198
  }
1199
+ return void 0;
1246
1200
  }
1247
- });
1248
-
1249
- // src/config-schema.js
1250
- var require_config_schema = __commonJS({
1251
- "src/config-schema.js"(exports2) {
1252
- "use strict";
1253
- Object.defineProperty(exports2, "__esModule", { value: true });
1254
- exports2.RUNTIME_DEFAULTS = exports2.bootstrapSchema = exports2.DEFAULT_DATA_PATH = void 0;
1255
- var zod_1 = require("zod");
1256
- exports2.DEFAULT_DATA_PATH = "camstack-data";
1257
- exports2.bootstrapSchema = zod_1.z.object({
1258
- /** Server mode: 'hub' (full server) or 'agent' (worker node) */
1259
- mode: zod_1.z.enum(["hub", "agent"]).default("hub"),
1260
- server: zod_1.z.object({
1261
- port: zod_1.z.number().default(4443),
1262
- host: zod_1.z.string().default("0.0.0.0"),
1263
- dataPath: zod_1.z.string().default(exports2.DEFAULT_DATA_PATH)
1264
- }).default({}),
1265
- auth: zod_1.z.object({
1266
- jwtSecret: zod_1.z.string().nullable().default(null),
1267
- adminUsername: zod_1.z.string().default("admin"),
1268
- adminPassword: zod_1.z.string().default(process.env.ADMIN_PASSWORD ?? "changeme")
1269
- }).default({}),
1270
- /** Hub connection config — only used when mode='agent' */
1271
- hub: zod_1.z.object({
1272
- url: zod_1.z.string().default("ws://localhost:4443/agent"),
1273
- token: zod_1.z.string().default("")
1274
- }).default({}),
1275
- /** Agent-specific config — only used when mode='agent' */
1276
- agent: zod_1.z.object({
1277
- name: zod_1.z.string().default(""),
1278
- /** Port for the agent status page (minimal HTML) */
1279
- statusPort: zod_1.z.number().default(4444)
1280
- }).default({}),
1281
- /** TLS configuration */
1282
- tls: zod_1.z.object({
1283
- /** Enable HTTPS (default: true) */
1284
- enabled: zod_1.z.boolean().default(true),
1285
- /** Path to custom cert file (PEM). If not set, auto-generates self-signed. */
1286
- certPath: zod_1.z.string().optional(),
1287
- /** Path to custom key file (PEM). Required if certPath is set. */
1288
- keyPath: zod_1.z.string().optional()
1289
- }).default({})
1290
- });
1291
- exports2.RUNTIME_DEFAULTS = {
1292
- "features.streaming": true,
1293
- "features.notifications": true,
1294
- "features.objectDetection": false,
1295
- "features.remoteAccess": true,
1296
- "features.agentCluster": false,
1297
- "features.smartHome": true,
1298
- "features.recordings": true,
1299
- "features.backup": true,
1300
- "features.repl": true,
1301
- "retention.detectionEventsDays": 30,
1302
- "retention.audioLevelsDays": 7,
1303
- "logging.level": "info",
1304
- "logging.retentionDays": 30,
1305
- "eventBus.ringBufferSize": 1e4,
1306
- "storage.provider": "sqlite-storage",
1307
- "storage.locations": {
1308
- data: "camstack-data/data",
1309
- media: "camstack-data/media",
1310
- recordings: "camstack-data/recordings",
1311
- cache: "/tmp/camstack-cache",
1312
- logs: "camstack-data/logs",
1313
- models: "camstack-data/models"
1314
- },
1315
- "providers": [],
1316
- // Recording
1317
- "recording.segmentDurationSeconds": 4,
1318
- "recording.defaultRetentionDays": 30,
1319
- // Streaming ports are addon-specific (go2rtc owns its defaults)
1320
- // FFmpeg
1321
- "ffmpeg.binaryPath": "ffmpeg",
1322
- "ffmpeg.hwAccel": "auto",
1323
- "ffmpeg.threadCount": 0,
1324
- // Detection defaults
1325
- "detection.defaultMotionFps": 2,
1326
- "detection.defaultDetectionFps": 5,
1327
- "detection.defaultCooldownSeconds": 10,
1328
- "detection.defaultConfidenceThreshold": 0.4,
1329
- "detection.trackerMaxAgeFrames": 30,
1330
- "detection.trackerMinHits": 3,
1331
- "detection.trackerIouThreshold": 0.3,
1332
- // Backup retention is addon-specific (local-backup owns its default)
1333
- // Auth (runtime)
1334
- "auth.tokenExpiry": "24h"
1201
+ /**
1202
+ * Write a value to SQL system_settings.
1203
+ * Throws if the settings store is not yet wired.
1204
+ */
1205
+ set(key, value) {
1206
+ if (this.settingsStore === null) {
1207
+ throw new Error("[ConfigManager] SettingsStore not initialized -- call setSettingsStore() first");
1208
+ }
1209
+ this.settingsStore.setSystem(key, value);
1210
+ }
1211
+ /**
1212
+ * Bulk-read all system_settings keys that belong to a logical section.
1213
+ * A "section" is the first segment of a dot-notation key (e.g. 'features', 'logging').
1214
+ */
1215
+ getSection(section) {
1216
+ if (this.settingsStore !== null) {
1217
+ const nested = this.getNestedFromSystemSettings(section);
1218
+ if (nested !== void 0) return nested;
1219
+ }
1220
+ const bootstrapValue = this.bootstrapConfig[section];
1221
+ if (bootstrapValue !== void 0 && bootstrapValue !== null && typeof bootstrapValue === "object") {
1222
+ return bootstrapValue;
1223
+ }
1224
+ return this.getFromRuntimeDefaults(section) ?? {};
1225
+ }
1226
+ /**
1227
+ * Bulk-write a section of runtime settings to SQL system_settings.
1228
+ * Each entry in `data` is stored as `section.key`.
1229
+ */
1230
+ setSection(section, data) {
1231
+ if (this.settingsStore === null) {
1232
+ throw new Error("[ConfigManager] SettingsStore not initialized -- call setSettingsStore() first");
1233
+ }
1234
+ for (const [key, value] of Object.entries(data)) {
1235
+ this.settingsStore.setSystem(`${section}.${key}`, value);
1236
+ }
1237
+ }
1238
+ // ---------------------------------------------------------------------------
1239
+ // Addon / Provider / Device scoped config
1240
+ // ---------------------------------------------------------------------------
1241
+ /** Read all config for an addon from addon_settings. */
1242
+ getAddonConfig(addonId) {
1243
+ if (this.settingsStore !== null) {
1244
+ return this.settingsStore.getAllAddon(addonId);
1245
+ }
1246
+ return this.getFromBootstrap(`addons.${addonId}`) ?? {};
1247
+ }
1248
+ /** Write (bulk-replace) config for an addon to addon_settings. */
1249
+ setAddonConfig(addonId, config) {
1250
+ if (this.settingsStore === null) {
1251
+ throw new Error("[ConfigManager] SettingsStore not initialized -- call setSettingsStore() first");
1252
+ }
1253
+ this.settingsStore.setAllAddon(addonId, config);
1254
+ }
1255
+ /** Read all config for a provider from provider_settings. */
1256
+ getProviderConfig(providerId) {
1257
+ if (this.settingsStore !== null) {
1258
+ return this.settingsStore.getAllProvider(providerId);
1259
+ }
1260
+ return {};
1261
+ }
1262
+ /** Write (upsert) a single key for a provider to provider_settings. */
1263
+ setProviderConfig(providerId, key, value) {
1264
+ if (this.settingsStore === null) {
1265
+ throw new Error("[ConfigManager] SettingsStore not initialized -- call setSettingsStore() first");
1266
+ }
1267
+ this.settingsStore.setProvider(providerId, key, value);
1268
+ }
1269
+ /** Read all config for a device from device_settings. */
1270
+ getDeviceConfig(deviceId) {
1271
+ if (this.settingsStore !== null) {
1272
+ return this.settingsStore.getAllDevice(deviceId);
1273
+ }
1274
+ return {};
1275
+ }
1276
+ /** Write (upsert) a single key for a device to device_settings. */
1277
+ setDeviceConfig(deviceId, key, value) {
1278
+ if (this.settingsStore === null) {
1279
+ throw new Error("[ConfigManager] SettingsStore not initialized -- call setSettingsStore() first");
1280
+ }
1281
+ this.settingsStore.setDevice(deviceId, key, value);
1282
+ }
1283
+ /** Get a value from the parsed bootstrap config */
1284
+ getBootstrap(path5) {
1285
+ return this.getFromBootstrap(path5);
1286
+ }
1287
+ /** Features accessor -- reads from SQL when available, falls back to RUNTIME_DEFAULTS */
1288
+ get features() {
1289
+ const g = (key) => this.get(`features.${key}`) ?? RUNTIME_DEFAULTS[`features.${key}`];
1290
+ return {
1291
+ streaming: g("streaming"),
1292
+ notifications: g("notifications"),
1293
+ objectDetection: g("objectDetection"),
1294
+ remoteAccess: g("remoteAccess"),
1295
+ agentCluster: g("agentCluster"),
1296
+ smartHome: g("smartHome"),
1297
+ recordings: g("recordings"),
1298
+ backup: g("backup"),
1299
+ repl: g("repl")
1335
1300
  };
1336
1301
  }
1337
- });
1338
-
1339
- // src/config-manager.js
1340
- var require_config_manager = __commonJS({
1341
- "src/config-manager.js"(exports2) {
1342
- "use strict";
1343
- var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
1344
- if (k2 === void 0) k2 = k;
1345
- var desc = Object.getOwnPropertyDescriptor(m, k);
1346
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
1347
- desc = { enumerable: true, get: function() {
1348
- return m[k];
1349
- } };
1350
- }
1351
- Object.defineProperty(o, k2, desc);
1352
- }) : (function(o, m, k, k2) {
1353
- if (k2 === void 0) k2 = k;
1354
- o[k2] = m[k];
1355
- }));
1356
- var __setModuleDefault = exports2 && exports2.__setModuleDefault || (Object.create ? (function(o, v) {
1357
- Object.defineProperty(o, "default", { enumerable: true, value: v });
1358
- }) : function(o, v) {
1359
- o["default"] = v;
1360
- });
1361
- var __importStar = exports2 && exports2.__importStar || /* @__PURE__ */ (function() {
1362
- var ownKeys = function(o) {
1363
- ownKeys = Object.getOwnPropertyNames || function(o2) {
1364
- var ar = [];
1365
- for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
1366
- return ar;
1367
- };
1368
- return ownKeys(o);
1369
- };
1370
- return function(mod) {
1371
- if (mod && mod.__esModule) return mod;
1372
- var result = {};
1373
- if (mod != null) {
1374
- for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
1375
- }
1376
- __setModuleDefault(result, mod);
1377
- return result;
1378
- };
1379
- })();
1380
- Object.defineProperty(exports2, "__esModule", { value: true });
1381
- exports2.ConfigManager = void 0;
1382
- var fs = __importStar(require("fs"));
1383
- var yaml = __importStar(require("js-yaml"));
1384
- var config_schema_js_1 = require_config_schema();
1385
- var ENV_VAR_MAP = {
1386
- CAMSTACK_PORT: "server.port",
1387
- CAMSTACK_HOST: "server.host",
1388
- CAMSTACK_DATA: "server.dataPath",
1389
- CAMSTACK_JWT_SECRET: "auth.jwtSecret",
1390
- CAMSTACK_ADMIN_USER: "auth.adminUsername",
1391
- CAMSTACK_ADMIN_PASS: "auth.adminPassword"
1302
+ /**
1303
+ * Returns a merged view of bootstrap config + runtime defaults for backward compat.
1304
+ */
1305
+ get raw() {
1306
+ const features = {
1307
+ streaming: RUNTIME_DEFAULTS["features.streaming"],
1308
+ notifications: RUNTIME_DEFAULTS["features.notifications"],
1309
+ objectDetection: RUNTIME_DEFAULTS["features.objectDetection"],
1310
+ remoteAccess: RUNTIME_DEFAULTS["features.remoteAccess"],
1311
+ agentCluster: RUNTIME_DEFAULTS["features.agentCluster"],
1312
+ smartHome: RUNTIME_DEFAULTS["features.smartHome"],
1313
+ recordings: RUNTIME_DEFAULTS["features.recordings"],
1314
+ backup: RUNTIME_DEFAULTS["features.backup"],
1315
+ repl: RUNTIME_DEFAULTS["features.repl"]
1392
1316
  };
1393
- var ConfigManager2 = class _ConfigManager {
1394
- configPath;
1395
- // Non-readonly so update() can sync the in-memory view after a write.
1396
- bootstrapConfig;
1397
- settingsStore = null;
1398
- constructor(configPath) {
1399
- this.configPath = configPath;
1400
- const rawYaml = this.loadYaml();
1401
- const merged = this.applyEnvOverrides(rawYaml);
1402
- this.bootstrapConfig = config_schema_js_1.bootstrapSchema.parse(merged);
1403
- this.warnDefaultCredentials();
1404
- }
1405
- /** Called by main.ts after the SQLite DB is ready (Phase 2). */
1406
- setSettingsStore(store) {
1407
- this.settingsStore = store;
1408
- }
1409
- /**
1410
- * Get a config value by dot-notation path.
1411
- * Priority: bootstrap config -> SQL system_settings -> RUNTIME_DEFAULTS fallback.
1412
- */
1413
- get(path) {
1414
- const bootstrapValue = this.getFromBootstrap(path);
1415
- if (bootstrapValue !== void 0) {
1416
- return bootstrapValue;
1417
- }
1418
- if (this.settingsStore !== null) {
1419
- const sqlValue = this.settingsStore.getSystem(path);
1420
- if (sqlValue !== void 0) {
1421
- return sqlValue;
1422
- }
1423
- const sqlNested = this.getNestedFromSystemSettings(path);
1424
- if (sqlNested !== void 0) {
1425
- return sqlNested;
1426
- }
1427
- }
1428
- if (path in config_schema_js_1.RUNTIME_DEFAULTS) {
1429
- return config_schema_js_1.RUNTIME_DEFAULTS[path];
1430
- }
1431
- const nested = this.getFromRuntimeDefaults(path);
1432
- if (nested !== void 0) {
1433
- return nested;
1434
- }
1435
- return void 0;
1436
- }
1437
- /**
1438
- * Write a value to SQL system_settings.
1439
- * Throws if the settings store is not yet wired.
1440
- */
1441
- set(key, value) {
1442
- if (this.settingsStore === null) {
1443
- throw new Error("[ConfigManager] SettingsStore not initialized -- call setSettingsStore() first");
1444
- }
1445
- this.settingsStore.setSystem(key, value);
1446
- }
1447
- /**
1448
- * Bulk-read all system_settings keys that belong to a logical section.
1449
- * A "section" is the first segment of a dot-notation key (e.g. 'features', 'logging').
1450
- */
1451
- getSection(section) {
1452
- if (this.settingsStore !== null) {
1453
- const nested = this.getNestedFromSystemSettings(section);
1454
- if (nested !== void 0)
1455
- return nested;
1456
- }
1457
- const bootstrapValue = this.bootstrapConfig[section];
1458
- if (bootstrapValue !== void 0 && bootstrapValue !== null && typeof bootstrapValue === "object") {
1459
- return bootstrapValue;
1460
- }
1461
- return this.getFromRuntimeDefaults(section) ?? {};
1462
- }
1463
- /**
1464
- * Bulk-write a section of runtime settings to SQL system_settings.
1465
- * Each entry in `data` is stored as `section.key`.
1466
- */
1467
- setSection(section, data) {
1468
- if (this.settingsStore === null) {
1469
- throw new Error("[ConfigManager] SettingsStore not initialized -- call setSettingsStore() first");
1470
- }
1471
- for (const [key, value] of Object.entries(data)) {
1472
- this.settingsStore.setSystem(`${section}.${key}`, value);
1473
- }
1474
- }
1475
- // ---------------------------------------------------------------------------
1476
- // Addon / Provider / Device scoped config
1477
- // ---------------------------------------------------------------------------
1478
- /** Read all config for an addon from addon_settings. */
1479
- getAddonConfig(addonId) {
1480
- if (this.settingsStore !== null) {
1481
- return this.settingsStore.getAllAddon(addonId);
1482
- }
1483
- return this.getFromBootstrap(`addons.${addonId}`) ?? {};
1484
- }
1485
- /** Write (bulk-replace) config for an addon to addon_settings. */
1486
- setAddonConfig(addonId, config) {
1487
- if (this.settingsStore === null) {
1488
- throw new Error("[ConfigManager] SettingsStore not initialized -- call setSettingsStore() first");
1489
- }
1490
- this.settingsStore.setAllAddon(addonId, config);
1491
- }
1492
- /** Read all config for a provider from provider_settings. */
1493
- getProviderConfig(providerId) {
1494
- if (this.settingsStore !== null) {
1495
- return this.settingsStore.getAllProvider(providerId);
1496
- }
1497
- return {};
1498
- }
1499
- /** Write (upsert) a single key for a provider to provider_settings. */
1500
- setProviderConfig(providerId, key, value) {
1501
- if (this.settingsStore === null) {
1502
- throw new Error("[ConfigManager] SettingsStore not initialized -- call setSettingsStore() first");
1503
- }
1504
- this.settingsStore.setProvider(providerId, key, value);
1505
- }
1506
- /** Read all config for a device from device_settings. */
1507
- getDeviceConfig(deviceId) {
1508
- if (this.settingsStore !== null) {
1509
- return this.settingsStore.getAllDevice(deviceId);
1510
- }
1511
- return {};
1512
- }
1513
- /** Write (upsert) a single key for a device to device_settings. */
1514
- setDeviceConfig(deviceId, key, value) {
1515
- if (this.settingsStore === null) {
1516
- throw new Error("[ConfigManager] SettingsStore not initialized -- call setSettingsStore() first");
1517
- }
1518
- this.settingsStore.setDevice(deviceId, key, value);
1519
- }
1520
- /** Get a value from the parsed bootstrap config */
1521
- getBootstrap(path) {
1522
- return this.getFromBootstrap(path);
1523
- }
1524
- /** Features accessor -- reads from SQL when available, falls back to RUNTIME_DEFAULTS */
1525
- get features() {
1526
- const g = (key) => this.get(`features.${key}`) ?? config_schema_js_1.RUNTIME_DEFAULTS[`features.${key}`];
1527
- return {
1528
- streaming: g("streaming"),
1529
- notifications: g("notifications"),
1530
- objectDetection: g("objectDetection"),
1531
- remoteAccess: g("remoteAccess"),
1532
- agentCluster: g("agentCluster"),
1533
- smartHome: g("smartHome"),
1534
- recordings: g("recordings"),
1535
- backup: g("backup"),
1536
- repl: g("repl")
1537
- };
1538
- }
1539
- /**
1540
- * Returns a merged view of bootstrap config + runtime defaults for backward compat.
1541
- */
1542
- get raw() {
1543
- const features = {
1544
- streaming: config_schema_js_1.RUNTIME_DEFAULTS["features.streaming"],
1545
- notifications: config_schema_js_1.RUNTIME_DEFAULTS["features.notifications"],
1546
- objectDetection: config_schema_js_1.RUNTIME_DEFAULTS["features.objectDetection"],
1547
- remoteAccess: config_schema_js_1.RUNTIME_DEFAULTS["features.remoteAccess"],
1548
- agentCluster: config_schema_js_1.RUNTIME_DEFAULTS["features.agentCluster"],
1549
- smartHome: config_schema_js_1.RUNTIME_DEFAULTS["features.smartHome"],
1550
- recordings: config_schema_js_1.RUNTIME_DEFAULTS["features.recordings"],
1551
- backup: config_schema_js_1.RUNTIME_DEFAULTS["features.backup"],
1552
- repl: config_schema_js_1.RUNTIME_DEFAULTS["features.repl"]
1553
- };
1554
- return {
1555
- ...this.bootstrapConfig,
1556
- features,
1557
- storage: config_schema_js_1.RUNTIME_DEFAULTS["storage.locations"] !== void 0 ? {
1558
- provider: config_schema_js_1.RUNTIME_DEFAULTS["storage.provider"],
1559
- locations: config_schema_js_1.RUNTIME_DEFAULTS["storage.locations"]
1560
- } : { provider: "sqlite-storage", locations: {} },
1561
- logging: {
1562
- level: config_schema_js_1.RUNTIME_DEFAULTS["logging.level"],
1563
- retentionDays: config_schema_js_1.RUNTIME_DEFAULTS["logging.retentionDays"]
1564
- },
1565
- eventBus: {
1566
- ringBufferSize: config_schema_js_1.RUNTIME_DEFAULTS["eventBus.ringBufferSize"]
1567
- },
1568
- retention: {
1569
- detectionEventsDays: config_schema_js_1.RUNTIME_DEFAULTS["retention.detectionEventsDays"],
1570
- audioLevelsDays: config_schema_js_1.RUNTIME_DEFAULTS["retention.audioLevelsDays"]
1571
- },
1572
- providers: config_schema_js_1.RUNTIME_DEFAULTS["providers"]
1573
- };
1574
- }
1575
- /** Sections that live in config.yaml. Everything else goes to SQL. */
1576
- static BOOTSTRAP_SECTIONS = /* @__PURE__ */ new Set(["server", "auth", "mode"]);
1577
- /**
1578
- * Atomically update one top-level section of config.yaml and sync in-memory.
1579
- * Only bootstrap sections (server, auth, mode) are written to YAML.
1580
- * Runtime settings must use setSection() which writes to SQL.
1581
- */
1582
- update(section, data) {
1583
- if (!_ConfigManager.BOOTSTRAP_SECTIONS.has(section)) {
1584
- throw new Error(`[ConfigManager] Section "${section}" is a runtime setting \u2014 use setSection() to write to DB, not update() which writes to config.yaml`);
1585
- }
1586
- let raw = {};
1587
- if (fs.existsSync(this.configPath)) {
1588
- raw = yaml.load(fs.readFileSync(this.configPath, "utf-8")) ?? {};
1589
- }
1590
- const existing = raw[section] ?? {};
1591
- raw[section] = { ...existing, ...data };
1592
- const validation = config_schema_js_1.bootstrapSchema.safeParse(raw);
1593
- if (!validation.success) {
1594
- throw new Error(`[ConfigManager] Invalid config update for section "${section}": ${validation.error.message}`);
1595
- }
1596
- const tmpPath = `${this.configPath}.tmp`;
1597
- fs.writeFileSync(tmpPath, yaml.dump(raw, { lineWidth: 120, indent: 2, quotingType: '"' }), "utf-8");
1598
- fs.renameSync(tmpPath, this.configPath);
1599
- this.bootstrapConfig = validation.data;
1600
- }
1601
- /**
1602
- * Deep-set a value in a nested plain object using a dot-notation path.
1603
- * Returns a new object (immutable).
1604
- */
1605
- setNested(obj, path, value) {
1606
- const [head, ...rest] = path.split(".");
1607
- if (!head)
1608
- return obj;
1609
- if (rest.length === 0) {
1610
- return { ...obj, [head]: value };
1611
- }
1612
- const child = obj[head] ?? {};
1613
- return { ...obj, [head]: this.setNested(child, rest.join("."), value) };
1614
- }
1615
- /**
1616
- * Apply env var overrides onto the raw YAML object.
1617
- * Only bootstrap-level env vars are applied.
1618
- */
1619
- applyEnvOverrides(raw) {
1620
- let result = { ...raw };
1621
- for (const [envKey, configPath] of Object.entries(ENV_VAR_MAP)) {
1622
- const envValue = process.env[envKey];
1623
- if (envValue === void 0 || envValue === "")
1624
- continue;
1625
- const coerced = configPath === "server.port" ? Number(envValue) : envValue;
1626
- result = this.setNested(result, configPath, coerced);
1627
- console.log(`[ConfigManager] Env override applied: ${envKey} \u2192 ${configPath}`);
1628
- }
1629
- return result;
1630
- }
1631
- loadYaml() {
1632
- if (!fs.existsSync(this.configPath)) {
1633
- console.warn(`[ConfigManager] Config file not found at: ${this.configPath}
1317
+ return {
1318
+ ...this.bootstrapConfig,
1319
+ features,
1320
+ storage: RUNTIME_DEFAULTS["storage.locations"] !== void 0 ? {
1321
+ provider: RUNTIME_DEFAULTS["storage.provider"],
1322
+ locations: RUNTIME_DEFAULTS["storage.locations"]
1323
+ } : { provider: "sqlite-storage", locations: {} },
1324
+ logging: {
1325
+ level: RUNTIME_DEFAULTS["logging.level"],
1326
+ retentionDays: RUNTIME_DEFAULTS["logging.retentionDays"]
1327
+ },
1328
+ eventBus: {
1329
+ ringBufferSize: RUNTIME_DEFAULTS["eventBus.ringBufferSize"]
1330
+ },
1331
+ retention: {
1332
+ detectionEventsDays: RUNTIME_DEFAULTS["retention.detectionEventsDays"],
1333
+ audioLevelsDays: RUNTIME_DEFAULTS["retention.audioLevelsDays"]
1334
+ },
1335
+ providers: RUNTIME_DEFAULTS["providers"]
1336
+ };
1337
+ }
1338
+ /** Sections that live in config.yaml. Everything else goes to SQL. */
1339
+ static BOOTSTRAP_SECTIONS = /* @__PURE__ */ new Set(["server", "auth", "mode"]);
1340
+ /**
1341
+ * Atomically update one top-level section of config.yaml and sync in-memory.
1342
+ * Only bootstrap sections (server, auth, mode) are written to YAML.
1343
+ * Runtime settings must use setSection() which writes to SQL.
1344
+ */
1345
+ update(section, data) {
1346
+ if (!_ConfigManager.BOOTSTRAP_SECTIONS.has(section)) {
1347
+ throw new Error(
1348
+ `[ConfigManager] Section "${section}" is a runtime setting \u2014 use setSection() to write to DB, not update() which writes to config.yaml`
1349
+ );
1350
+ }
1351
+ let raw = {};
1352
+ if (fs5.existsSync(this.configPath)) {
1353
+ raw = yaml.load(fs5.readFileSync(this.configPath, "utf-8")) ?? {};
1354
+ }
1355
+ const existing = raw[section] ?? {};
1356
+ raw[section] = { ...existing, ...data };
1357
+ const validation = bootstrapSchema.safeParse(raw);
1358
+ if (!validation.success) {
1359
+ throw new Error(`[ConfigManager] Invalid config update for section "${section}": ${validation.error.message}`);
1360
+ }
1361
+ const tmpPath = `${this.configPath}.tmp`;
1362
+ fs5.writeFileSync(tmpPath, yaml.dump(raw, { lineWidth: 120, indent: 2, quotingType: '"' }), "utf-8");
1363
+ fs5.renameSync(tmpPath, this.configPath);
1364
+ this.bootstrapConfig = validation.data;
1365
+ }
1366
+ /**
1367
+ * Deep-set a value in a nested plain object using a dot-notation path.
1368
+ * Returns a new object (immutable).
1369
+ */
1370
+ setNested(obj, path5, value) {
1371
+ const [head, ...rest] = path5.split(".");
1372
+ if (!head) return obj;
1373
+ if (rest.length === 0) {
1374
+ return { ...obj, [head]: value };
1375
+ }
1376
+ const child = obj[head] ?? {};
1377
+ return { ...obj, [head]: this.setNested(child, rest.join("."), value) };
1378
+ }
1379
+ /**
1380
+ * Apply env var overrides onto the raw YAML object.
1381
+ * Only bootstrap-level env vars are applied.
1382
+ */
1383
+ applyEnvOverrides(raw) {
1384
+ let result = { ...raw };
1385
+ for (const [envKey, configPath] of Object.entries(ENV_VAR_MAP)) {
1386
+ const envValue = process.env[envKey];
1387
+ if (envValue === void 0 || envValue === "") continue;
1388
+ const coerced = configPath === "server.port" ? Number(envValue) : envValue;
1389
+ result = this.setNested(result, configPath, coerced);
1390
+ console.log(`[ConfigManager] Env override applied: ${envKey} \u2192 ${configPath}`);
1391
+ }
1392
+ return result;
1393
+ }
1394
+ loadYaml() {
1395
+ if (!fs5.existsSync(this.configPath)) {
1396
+ console.warn(
1397
+ `[ConfigManager] Config file not found at: ${this.configPath}
1634
1398
  \u2192 Using built-in defaults. Set CONFIG_PATH env var or create the file.
1635
- \u2192 Example path from project root: ./server/backend/data/config.yaml`);
1636
- return {};
1637
- }
1638
- const content = fs.readFileSync(this.configPath, "utf-8");
1639
- const parsed = yaml.load(content) ?? {};
1640
- console.log(`[ConfigManager] Loaded config from: ${this.configPath}`);
1641
- return parsed;
1642
- }
1643
- warnDefaultCredentials() {
1644
- if (this.bootstrapConfig.auth.adminPassword === "changeme") {
1645
- console.warn(`[ConfigManager] Warning: Using default admin password "changeme". Set auth.adminPassword in your config.yaml or the ADMIN_PASSWORD env var.`);
1646
- }
1647
- }
1648
- getFromBootstrap(path) {
1649
- const keys = path.split(".");
1650
- let current = this.bootstrapConfig;
1651
- for (const key of keys) {
1652
- if (current === null || current === void 0 || typeof current !== "object") {
1653
- return void 0;
1654
- }
1655
- current = current[key];
1656
- }
1657
- return current;
1399
+ \u2192 Example path from project root: ./server/backend/data/config.yaml`
1400
+ );
1401
+ return {};
1402
+ }
1403
+ const content = fs5.readFileSync(this.configPath, "utf-8");
1404
+ const parsed = yaml.load(content) ?? {};
1405
+ console.log(`[ConfigManager] Loaded config from: ${this.configPath}`);
1406
+ return parsed;
1407
+ }
1408
+ warnDefaultCredentials() {
1409
+ if (this.bootstrapConfig.auth.adminPassword === "changeme") {
1410
+ console.warn(
1411
+ `[ConfigManager] Warning: Using default admin password "changeme". Set auth.adminPassword in your config.yaml or the ADMIN_PASSWORD env var.`
1412
+ );
1413
+ }
1414
+ }
1415
+ getFromBootstrap(path5) {
1416
+ const keys = path5.split(".");
1417
+ let current = this.bootstrapConfig;
1418
+ for (const key of keys) {
1419
+ if (current === null || current === void 0 || typeof current !== "object") {
1420
+ return void 0;
1658
1421
  }
1659
- getFromRuntimeDefaults(path) {
1660
- const prefix = path + ".";
1661
- const result = {};
1662
- let found = false;
1663
- for (const [key, value] of Object.entries(config_schema_js_1.RUNTIME_DEFAULTS)) {
1664
- if (key.startsWith(prefix)) {
1665
- const subKey = key.slice(prefix.length);
1666
- result[subKey] = value;
1667
- found = true;
1668
- }
1669
- }
1670
- return found ? result : void 0;
1422
+ current = current[key];
1423
+ }
1424
+ return current;
1425
+ }
1426
+ getFromRuntimeDefaults(path5) {
1427
+ const prefix = path5 + ".";
1428
+ const result = {};
1429
+ let found = false;
1430
+ for (const [key, value] of Object.entries(RUNTIME_DEFAULTS)) {
1431
+ if (key.startsWith(prefix)) {
1432
+ const subKey = key.slice(prefix.length);
1433
+ result[subKey] = value;
1434
+ found = true;
1671
1435
  }
1672
- /**
1673
- * Perform a prefix-based nested lookup against SQL system_settings.
1674
- * e.g. path='features' matches keys 'features.streaming', 'features.notifications', etc.
1675
- * Returns an object keyed by the sub-key, or undefined if nothing is found.
1676
- */
1677
- getNestedFromSystemSettings(path) {
1678
- if (this.settingsStore === null)
1679
- return void 0;
1680
- const all = this.settingsStore.getAllSystem();
1681
- const prefix = path + ".";
1682
- const result = {};
1683
- let found = false;
1684
- for (const [key, value] of Object.entries(all)) {
1685
- if (key.startsWith(prefix)) {
1686
- result[key.slice(prefix.length)] = value;
1687
- found = true;
1688
- }
1689
- }
1690
- return found ? result : void 0;
1436
+ }
1437
+ return found ? result : void 0;
1438
+ }
1439
+ /**
1440
+ * Perform a prefix-based nested lookup against SQL system_settings.
1441
+ * e.g. path='features' matches keys 'features.streaming', 'features.notifications', etc.
1442
+ * Returns an object keyed by the sub-key, or undefined if nothing is found.
1443
+ */
1444
+ getNestedFromSystemSettings(path5) {
1445
+ if (this.settingsStore === null) return void 0;
1446
+ const all = this.settingsStore.getAllSystem();
1447
+ const prefix = path5 + ".";
1448
+ const result = {};
1449
+ let found = false;
1450
+ for (const [key, value] of Object.entries(all)) {
1451
+ if (key.startsWith(prefix)) {
1452
+ result[key.slice(prefix.length)] = value;
1453
+ found = true;
1691
1454
  }
1692
- };
1693
- exports2.ConfigManager = ConfigManager2;
1455
+ }
1456
+ return found ? result : void 0;
1694
1457
  }
1695
- });
1458
+ };
1696
1459
 
1697
- // src/worker/addon-worker-host.js
1698
- var require_addon_worker_host = __commonJS({
1699
- "src/worker/addon-worker-host.js"(exports2) {
1700
- "use strict";
1701
- Object.defineProperty(exports2, "__esModule", { value: true });
1702
- exports2.AddonWorkerHost = void 0;
1703
- var node_child_process_1 = require("child_process");
1704
- var INFRA_ADDON_IDS = /* @__PURE__ */ new Set([
1705
- "filesystem-storage",
1706
- "sqlite-settings",
1707
- "winston-logging"
1708
- ]);
1709
- var AddonWorkerHost2 = class {
1710
- workers = /* @__PURE__ */ new Map();
1711
- options;
1712
- heartbeatTimer = null;
1713
- /** Set of addons that failed to fork and fell back to in-process */
1714
- fallbackInProcess = /* @__PURE__ */ new Set();
1715
- constructor(options) {
1716
- this.options = {
1717
- workerEntryPath: options.workerEntryPath,
1718
- devMode: options.devMode ?? false,
1719
- forceInProcess: options.forceInProcess ?? process.env.CAMSTACK_FORCE_INPROCESS === "true",
1720
- heartbeatIntervalMs: options.heartbeatIntervalMs ?? 5e3,
1721
- heartbeatTimeoutMs: options.heartbeatTimeoutMs ?? 3e4,
1722
- maxCrashesInWindow: options.maxCrashesInWindow ?? 3,
1723
- crashWindowMs: options.crashWindowMs ?? 6e4,
1724
- shutdownTimeoutMs: options.shutdownTimeoutMs ?? 1e4,
1725
- onWorkerLog: options.onWorkerLog
1726
- };
1727
- }
1728
- /** Check if an addon is infrastructure (must stay in-process) */
1729
- isInfraAddon(addonId) {
1730
- return INFRA_ADDON_IDS.has(addonId);
1731
- }
1732
- /** Check if an addon fell back to in-process after fork failure */
1733
- isFallbackInProcess(addonId) {
1734
- return this.fallbackInProcess.has(addonId);
1735
- }
1736
- /** Mark an addon as fallen back to in-process */
1737
- markFallbackInProcess(addonId) {
1738
- this.fallbackInProcess.add(addonId);
1739
- }
1740
- /**
1741
- * Determine if an addon should be forked.
1742
- * Default: fork everything except infra addons.
1743
- * Addons can opt out with `inProcess: true` in declaration.
1744
- * Emergency fallback: CAMSTACK_FORCE_INPROCESS=true disables all forking.
1745
- */
1746
- shouldFork(addonId, declaration) {
1747
- if (this.options.forceInProcess)
1748
- return false;
1749
- if (this.options.devMode)
1750
- return false;
1751
- if (this.isInfraAddon(addonId))
1752
- return false;
1753
- if (this.fallbackInProcess.has(addonId))
1754
- return false;
1755
- if (declaration?.inProcess === true)
1756
- return false;
1757
- if (declaration?.forkable !== true)
1758
- return false;
1759
- return true;
1760
- }
1761
- /** Fork a worker for an addon */
1762
- async forkWorker(addonId, addonDir, config, storagePaths, dataDir, locationPaths, workerToken) {
1763
- if (this.workers.has(addonId)) {
1764
- throw new Error(`Worker for addon "${addonId}" already exists`);
1765
- }
1766
- const workerEnv = {
1767
- PATH: process.env.PATH ?? "",
1768
- HOME: process.env.HOME ?? "",
1769
- NODE_ENV: process.env.NODE_ENV ?? "production",
1770
- NODE_TLS_REJECT_UNAUTHORIZED: "0",
1771
- // Accept self-signed cert
1772
- CAMSTACK_WORKER_HUB_URL: `wss://localhost:${process.env.CAMSTACK_PORT ?? "4443"}/trpc`,
1773
- CAMSTACK_WORKER_TOKEN: workerToken ?? "",
1774
- CAMSTACK_ADDON_ID: addonId,
1775
- CAMSTACK_ADDON_DIR: addonDir,
1776
- CAMSTACK_ADDON_CONFIG: JSON.stringify(config),
1777
- CAMSTACK_DATA_DIR: dataDir ?? `camstack-data/addons-data/${addonId}`,
1778
- CAMSTACK_LOCATION_PATHS: JSON.stringify(locationPaths ?? storagePaths)
1779
- };
1780
- const forkOptions = {
1781
- stdio: ["inherit", "inherit", "inherit", "ipc"],
1782
- execArgv: [],
1783
- env: workerEnv
1784
- };
1785
- const child = (0, node_child_process_1.fork)(this.options.workerEntryPath, [], forkOptions);
1786
- const worker = {
1460
+ // src/worker/addon-worker-host.ts
1461
+ var import_node_child_process3 = require("child_process");
1462
+ var INFRA_ADDON_IDS = /* @__PURE__ */ new Set([
1463
+ "filesystem-storage",
1464
+ "sqlite-settings",
1465
+ "winston-logging"
1466
+ ]);
1467
+ var AddonWorkerHost = class {
1468
+ workers = /* @__PURE__ */ new Map();
1469
+ options;
1470
+ heartbeatTimer = null;
1471
+ /** Set of addons that failed to fork and fell back to in-process */
1472
+ fallbackInProcess = /* @__PURE__ */ new Set();
1473
+ constructor(options) {
1474
+ this.options = {
1475
+ workerEntryPath: options.workerEntryPath,
1476
+ devMode: options.devMode ?? false,
1477
+ forceInProcess: options.forceInProcess ?? process.env.CAMSTACK_FORCE_INPROCESS === "true",
1478
+ heartbeatIntervalMs: options.heartbeatIntervalMs ?? 5e3,
1479
+ heartbeatTimeoutMs: options.heartbeatTimeoutMs ?? 3e4,
1480
+ maxCrashesInWindow: options.maxCrashesInWindow ?? 3,
1481
+ crashWindowMs: options.crashWindowMs ?? 6e4,
1482
+ shutdownTimeoutMs: options.shutdownTimeoutMs ?? 1e4,
1483
+ onWorkerLog: options.onWorkerLog
1484
+ };
1485
+ }
1486
+ /** Check if an addon is infrastructure (must stay in-process) */
1487
+ isInfraAddon(addonId) {
1488
+ return INFRA_ADDON_IDS.has(addonId);
1489
+ }
1490
+ /** Check if an addon fell back to in-process after fork failure */
1491
+ isFallbackInProcess(addonId) {
1492
+ return this.fallbackInProcess.has(addonId);
1493
+ }
1494
+ /** Mark an addon as fallen back to in-process */
1495
+ markFallbackInProcess(addonId) {
1496
+ this.fallbackInProcess.add(addonId);
1497
+ }
1498
+ /**
1499
+ * Determine if an addon should be forked.
1500
+ * Default: fork everything except infra addons.
1501
+ * Addons can opt out with `inProcess: true` in declaration.
1502
+ * Emergency fallback: CAMSTACK_FORCE_INPROCESS=true disables all forking.
1503
+ */
1504
+ shouldFork(addonId, declaration) {
1505
+ if (this.options.forceInProcess) return false;
1506
+ if (this.options.devMode) return false;
1507
+ if (this.isInfraAddon(addonId)) return false;
1508
+ if (this.fallbackInProcess.has(addonId)) return false;
1509
+ if (declaration?.inProcess === true) return false;
1510
+ if (declaration?.forkable !== true) return false;
1511
+ return true;
1512
+ }
1513
+ /** Fork a worker for an addon */
1514
+ async forkWorker(addonId, addonDir, config, storagePaths, dataDir, locationPaths, workerToken) {
1515
+ if (this.workers.has(addonId)) {
1516
+ throw new Error(`Worker for addon "${addonId}" already exists`);
1517
+ }
1518
+ const workerEnv = {
1519
+ PATH: process.env.PATH ?? "",
1520
+ HOME: process.env.HOME ?? "",
1521
+ NODE_ENV: process.env.NODE_ENV ?? "production",
1522
+ NODE_TLS_REJECT_UNAUTHORIZED: "0",
1523
+ // Accept self-signed cert
1524
+ CAMSTACK_WORKER_HUB_URL: `wss://localhost:${process.env.CAMSTACK_PORT ?? "4443"}/trpc`,
1525
+ CAMSTACK_WORKER_TOKEN: workerToken ?? "",
1526
+ CAMSTACK_ADDON_ID: addonId,
1527
+ CAMSTACK_ADDON_DIR: addonDir,
1528
+ CAMSTACK_ADDON_CONFIG: JSON.stringify(config),
1529
+ CAMSTACK_DATA_DIR: dataDir ?? `camstack-data/addons-data/${addonId}`,
1530
+ CAMSTACK_LOCATION_PATHS: JSON.stringify(locationPaths ?? storagePaths)
1531
+ };
1532
+ const forkOptions = {
1533
+ stdio: ["inherit", "inherit", "inherit", "ipc"],
1534
+ execArgv: [],
1535
+ env: workerEnv
1536
+ };
1537
+ const child = (0, import_node_child_process3.fork)(this.options.workerEntryPath, [], forkOptions);
1538
+ const worker = {
1539
+ addonId,
1540
+ process: child,
1541
+ state: "starting",
1542
+ startedAt: Date.now(),
1543
+ lastHeartbeat: Date.now(),
1544
+ restartCount: 0,
1545
+ crashTimestamps: [],
1546
+ cpuPercent: 0,
1547
+ memoryRss: 0,
1548
+ workerToken,
1549
+ forkConfig: { addonDir, config, storagePaths, dataDir, locationPaths, workerToken }
1550
+ };
1551
+ this.workers.set(addonId, worker);
1552
+ child.on("message", (msg) => {
1553
+ if (msg.type === "LOG" && this.options.onWorkerLog) {
1554
+ this.options.onWorkerLog({
1787
1555
  addonId,
1788
- process: child,
1789
- state: "starting",
1790
- startedAt: Date.now(),
1791
- lastHeartbeat: Date.now(),
1792
- restartCount: 0,
1793
- crashTimestamps: [],
1794
- cpuPercent: 0,
1795
- memoryRss: 0,
1796
- workerToken
1797
- };
1798
- this.workers.set(addonId, worker);
1799
- child.on("message", (msg) => {
1800
- if (msg.type === "LOG" && this.options.onWorkerLog) {
1801
- this.options.onWorkerLog({
1802
- addonId,
1803
- level: msg.level,
1804
- message: msg.message,
1805
- scope: ["hub", addonId],
1806
- meta: msg.context
1807
- });
1808
- } else if (msg.type === "STATS") {
1809
- worker.cpuPercent = msg.cpu;
1810
- worker.memoryRss = msg.memory;
1811
- }
1812
- });
1813
- child.stdout?.on("data", (chunk) => {
1814
- const lines = chunk.toString().trim();
1815
- if (!lines)
1816
- return;
1817
- if (this.options.onWorkerLog) {
1818
- this.options.onWorkerLog({
1819
- addonId,
1820
- level: "info",
1821
- message: lines,
1822
- scope: ["hub", addonId, "__stdout"]
1823
- });
1824
- } else {
1825
- console.log(`[Worker:${addonId}] ${lines}`);
1826
- }
1827
- });
1828
- child.stderr?.on("data", (chunk) => {
1829
- const lines = chunk.toString().trim();
1830
- if (!lines)
1831
- return;
1832
- if (this.options.onWorkerLog) {
1833
- this.options.onWorkerLog({
1834
- addonId,
1835
- level: "error",
1836
- message: lines,
1837
- scope: ["hub", addonId, "__stderr"]
1838
- });
1839
- } else {
1840
- console.error(`[Worker:${addonId}:ERR] ${lines}`);
1841
- }
1842
- });
1843
- child.on("exit", (code) => {
1844
- this.handleWorkerExit(addonId, code, addonDir, config, storagePaths, dataDir, locationPaths, workerToken);
1845
- });
1846
- child.on("error", (err) => {
1847
- console.error(`[WorkerHost] Worker ${addonId} error:`, err.message);
1556
+ level: msg.level,
1557
+ message: msg.message,
1558
+ scope: ["hub", addonId],
1559
+ meta: msg.context
1848
1560
  });
1849
- await new Promise((resolve, reject) => {
1850
- const timeout = setTimeout(() => {
1851
- reject(new Error(`Worker ${addonId} did not become ready within 30s`));
1852
- }, 3e4);
1853
- const readyCheck = (msg) => {
1854
- if (msg.type === "READY") {
1855
- clearTimeout(timeout);
1856
- child.off("message", readyCheck);
1857
- worker.state = "running";
1858
- resolve();
1859
- }
1860
- };
1861
- child.on("message", readyCheck);
1862
- });
1863
- }
1864
- /** List all workers with stats */
1865
- listWorkers() {
1866
- return [...this.workers.values()].map((w) => ({
1867
- addonId: w.addonId,
1868
- pid: w.process.pid ?? 0,
1869
- state: w.state,
1870
- cpuPercent: w.cpuPercent,
1871
- memoryRss: w.memoryRss,
1872
- uptimeSeconds: Math.round((Date.now() - w.startedAt) / 1e3),
1873
- restartCount: w.restartCount,
1874
- subProcesses: []
1875
- }));
1876
- }
1877
- /** Gracefully shutdown a single worker by addon ID */
1878
- async shutdownWorkerById(addonId) {
1879
- const worker = this.workers.get(addonId);
1880
- if (!worker) {
1881
- throw new Error(`No worker found for addon "${addonId}"`);
1882
- }
1883
- await this.shutdownWorker(addonId, worker);
1884
- this.workers.delete(addonId);
1561
+ } else if (msg.type === "STATS") {
1562
+ worker.cpuPercent = msg.cpu;
1563
+ worker.memoryRss = msg.memory;
1885
1564
  }
1886
- /** Gracefully shutdown all workers */
1887
- async shutdownAll() {
1888
- if (this.heartbeatTimer) {
1889
- clearInterval(this.heartbeatTimer);
1890
- this.heartbeatTimer = null;
1891
- }
1892
- const shutdowns = [...this.workers.entries()].map(([addonId, worker]) => this.shutdownWorker(addonId, worker));
1893
- await Promise.allSettled(shutdowns);
1894
- this.workers.clear();
1895
- }
1896
- /** Start heartbeat monitoring — workers now report heartbeat via tRPC, not IPC PING */
1897
- startHeartbeatMonitoring() {
1898
- this.heartbeatTimer = setInterval(() => {
1899
- const now = Date.now();
1900
- for (const [addonId, worker] of this.workers) {
1901
- if (worker.state !== "running")
1902
- continue;
1903
- if (now - worker.lastHeartbeat > this.options.heartbeatTimeoutMs) {
1904
- console.warn(`[WorkerHost] Worker ${addonId} heartbeat timeout \u2014 marking unresponsive`);
1905
- worker.state = "crashed";
1906
- worker.process.kill("SIGKILL");
1907
- }
1908
- }
1909
- }, this.options.heartbeatIntervalMs);
1565
+ });
1566
+ child.stdout?.on("data", (chunk) => {
1567
+ const lines = chunk.toString().trim();
1568
+ if (!lines) return;
1569
+ if (this.options.onWorkerLog) {
1570
+ this.options.onWorkerLog({
1571
+ addonId,
1572
+ level: "info",
1573
+ message: lines,
1574
+ scope: ["hub", addonId, "__stdout"]
1575
+ });
1576
+ } else {
1577
+ console.log(`[Worker:${addonId}] ${lines}`);
1910
1578
  }
1911
- /** Update heartbeat timestamp for a worker (called when agent.heartbeat arrives) */
1912
- updateWorkerHeartbeat(addonId, cpuPercent, memoryRss) {
1913
- const worker = this.workers.get(addonId);
1914
- if (!worker)
1915
- return;
1916
- worker.lastHeartbeat = Date.now();
1917
- if (cpuPercent !== void 0)
1918
- worker.cpuPercent = cpuPercent;
1919
- if (memoryRss !== void 0)
1920
- worker.memoryRss = memoryRss;
1579
+ });
1580
+ child.stderr?.on("data", (chunk) => {
1581
+ const lines = chunk.toString().trim();
1582
+ if (!lines) return;
1583
+ if (this.options.onWorkerLog) {
1584
+ this.options.onWorkerLog({
1585
+ addonId,
1586
+ level: "error",
1587
+ message: lines,
1588
+ scope: ["hub", addonId, "__stderr"]
1589
+ });
1590
+ } else {
1591
+ console.error(`[Worker:${addonId}:ERR] ${lines}`);
1921
1592
  }
1922
- handleWorkerExit(addonId, code, addonDir, config, storagePaths, dataDir, locationPaths, workerToken) {
1923
- const worker = this.workers.get(addonId);
1924
- if (!worker)
1925
- return;
1926
- if (worker.state === "stopping") {
1927
- worker.state = "stopped";
1928
- return;
1929
- }
1930
- worker.state = "crashed";
1931
- console.error(`[WorkerHost] Worker ${addonId} exited with code ${code}`);
1932
- const now = Date.now();
1933
- worker.crashTimestamps.push(now);
1934
- const recentCrashes = worker.crashTimestamps.filter((t) => now - t < this.options.crashWindowMs);
1935
- worker.crashTimestamps = recentCrashes;
1936
- if (recentCrashes.length >= this.options.maxCrashesInWindow) {
1937
- console.error(`[WorkerHost] Worker ${addonId} crashed ${recentCrashes.length} times in ${this.options.crashWindowMs}ms \u2014 not restarting`);
1938
- return;
1593
+ });
1594
+ child.on("exit", (code) => {
1595
+ this.handleWorkerExit(addonId, code, addonDir, config, storagePaths, dataDir, locationPaths, workerToken);
1596
+ });
1597
+ child.on("error", (err) => {
1598
+ console.error(`[WorkerHost] Worker ${addonId} error:`, err.message);
1599
+ });
1600
+ await new Promise((resolve3, reject) => {
1601
+ const timeout = setTimeout(() => {
1602
+ reject(new Error(`Worker ${addonId} did not become ready within 30s`));
1603
+ }, 3e4);
1604
+ const readyCheck = (msg) => {
1605
+ if (msg.type === "READY") {
1606
+ clearTimeout(timeout);
1607
+ child.off("message", readyCheck);
1608
+ worker.state = "running";
1609
+ resolve3();
1939
1610
  }
1940
- const backoff = Math.min(2e3 * (worker.restartCount + 1), 3e4);
1941
- console.log(`[WorkerHost] Restarting worker ${addonId} in ${backoff}ms`);
1611
+ };
1612
+ child.on("message", readyCheck);
1613
+ });
1614
+ }
1615
+ /** List all workers with stats */
1616
+ listWorkers() {
1617
+ return [...this.workers.values()].map((w) => ({
1618
+ addonId: w.addonId,
1619
+ pid: w.process.pid ?? 0,
1620
+ state: w.state,
1621
+ cpuPercent: w.cpuPercent,
1622
+ memoryRss: w.memoryRss,
1623
+ uptimeSeconds: Math.round((Date.now() - w.startedAt) / 1e3),
1624
+ restartCount: w.restartCount,
1625
+ subProcesses: []
1626
+ }));
1627
+ }
1628
+ /** Gracefully shutdown a single worker by addon ID */
1629
+ async shutdownWorkerById(addonId) {
1630
+ const worker = this.workers.get(addonId);
1631
+ if (!worker) {
1632
+ throw new Error(`No worker found for addon "${addonId}"`);
1633
+ }
1634
+ await this.shutdownWorker(addonId, worker);
1635
+ this.workers.delete(addonId);
1636
+ }
1637
+ /** Gracefully restart a single worker by addon ID (stop then re-fork with same config) */
1638
+ async restartWorker(addonId) {
1639
+ const worker = this.workers.get(addonId);
1640
+ if (!worker) {
1641
+ throw new Error(`No worker found for addon "${addonId}"`);
1642
+ }
1643
+ const forkConfig = worker.forkConfig;
1644
+ if (!forkConfig) {
1645
+ throw new Error(`No fork config stored for addon "${addonId}" \u2014 cannot restart`);
1646
+ }
1647
+ await this.shutdownWorker(addonId, worker);
1648
+ this.workers.delete(addonId);
1649
+ await this.forkWorker(
1650
+ addonId,
1651
+ forkConfig.addonDir,
1652
+ forkConfig.config,
1653
+ forkConfig.storagePaths,
1654
+ forkConfig.dataDir,
1655
+ forkConfig.locationPaths,
1656
+ forkConfig.workerToken
1657
+ );
1658
+ }
1659
+ /** Gracefully shutdown all workers */
1660
+ async shutdownAll() {
1661
+ if (this.heartbeatTimer) {
1662
+ clearInterval(this.heartbeatTimer);
1663
+ this.heartbeatTimer = null;
1664
+ }
1665
+ const shutdowns = [...this.workers.entries()].map(
1666
+ ([addonId, worker]) => this.shutdownWorker(addonId, worker)
1667
+ );
1668
+ await Promise.allSettled(shutdowns);
1669
+ this.workers.clear();
1670
+ }
1671
+ /** Start heartbeat monitoring — workers now report heartbeat via tRPC, not IPC PING */
1672
+ startHeartbeatMonitoring() {
1673
+ this.heartbeatTimer = setInterval(() => {
1674
+ const now = Date.now();
1675
+ for (const [addonId, worker] of this.workers) {
1676
+ if (worker.state !== "running") continue;
1677
+ if (now - worker.lastHeartbeat > this.options.heartbeatTimeoutMs) {
1678
+ console.warn(`[WorkerHost] Worker ${addonId} heartbeat timeout \u2014 marking unresponsive`);
1679
+ worker.state = "crashed";
1680
+ worker.process.kill("SIGKILL");
1681
+ }
1682
+ }
1683
+ }, this.options.heartbeatIntervalMs);
1684
+ }
1685
+ /** Update heartbeat timestamp for a worker (called when agent.heartbeat arrives) */
1686
+ updateWorkerHeartbeat(addonId, cpuPercent, memoryRss) {
1687
+ const worker = this.workers.get(addonId);
1688
+ if (!worker) return;
1689
+ worker.lastHeartbeat = Date.now();
1690
+ if (cpuPercent !== void 0) worker.cpuPercent = cpuPercent;
1691
+ if (memoryRss !== void 0) worker.memoryRss = memoryRss;
1692
+ }
1693
+ handleWorkerExit(addonId, code, addonDir, config, storagePaths, dataDir, locationPaths, workerToken) {
1694
+ const worker = this.workers.get(addonId);
1695
+ if (!worker) return;
1696
+ if (worker.state === "stopping") {
1697
+ worker.state = "stopped";
1698
+ return;
1699
+ }
1700
+ worker.state = "crashed";
1701
+ console.error(`[WorkerHost] Worker ${addonId} exited with code ${code}`);
1702
+ const now = Date.now();
1703
+ worker.crashTimestamps.push(now);
1704
+ const recentCrashes = worker.crashTimestamps.filter(
1705
+ (t) => now - t < this.options.crashWindowMs
1706
+ );
1707
+ worker.crashTimestamps = recentCrashes;
1708
+ if (recentCrashes.length >= this.options.maxCrashesInWindow) {
1709
+ console.error(`[WorkerHost] Worker ${addonId} crashed ${recentCrashes.length} times in ${this.options.crashWindowMs}ms \u2014 not restarting`);
1710
+ return;
1711
+ }
1712
+ const backoff = Math.min(2e3 * (worker.restartCount + 1), 3e4);
1713
+ console.log(`[WorkerHost] Restarting worker ${addonId} in ${backoff}ms`);
1714
+ setTimeout(() => {
1715
+ worker.restartCount++;
1716
+ this.workers.delete(addonId);
1717
+ this.forkWorker(addonId, addonDir, config, storagePaths, dataDir, locationPaths, workerToken).catch((err) => {
1718
+ console.error(`[WorkerHost] Failed to restart worker ${addonId}:`, err);
1719
+ });
1720
+ }, backoff);
1721
+ }
1722
+ async shutdownWorker(addonId, worker) {
1723
+ worker.state = "stopping";
1724
+ worker.process.kill("SIGTERM");
1725
+ await new Promise((resolve3) => {
1726
+ const timeout = setTimeout(() => {
1727
+ console.warn(`[WorkerHost] Worker ${addonId} did not exit within ${this.options.shutdownTimeoutMs}ms \u2014 SIGKILL`);
1728
+ worker.process.kill("SIGKILL");
1729
+ resolve3();
1730
+ }, this.options.shutdownTimeoutMs);
1731
+ worker.process.on("exit", () => {
1732
+ clearTimeout(timeout);
1733
+ resolve3();
1734
+ });
1735
+ });
1736
+ }
1737
+ };
1738
+
1739
+ // src/worker/worker-process-manager.ts
1740
+ var import_node_child_process4 = require("child_process");
1741
+ var WorkerProcessManager = class {
1742
+ constructor(sendToMain) {
1743
+ this.sendToMain = sendToMain;
1744
+ }
1745
+ processes = /* @__PURE__ */ new Map();
1746
+ async spawn(config) {
1747
+ const child = (0, import_node_child_process4.spawn)(config.command, [...config.args ?? []], {
1748
+ cwd: config.cwd,
1749
+ env: config.env ? { ...process.env, ...config.env } : void 0,
1750
+ stdio: ["pipe", "pipe", "pipe"]
1751
+ });
1752
+ const managed = new ManagedProcess(child, config, this.sendToMain);
1753
+ this.processes.set(child.pid, managed);
1754
+ this.sendToMain({
1755
+ type: "SUB_PROCESS_SPAWNED",
1756
+ pid: child.pid,
1757
+ name: config.name,
1758
+ command: config.command
1759
+ });
1760
+ child.on("exit", (code) => {
1761
+ this.sendToMain({
1762
+ type: "SUB_PROCESS_EXITED",
1763
+ pid: child.pid,
1764
+ code
1765
+ });
1766
+ if (config.autoRestart && managed.restartCount < (config.maxRestarts ?? 3)) {
1767
+ managed.restartCount++;
1942
1768
  setTimeout(() => {
1943
- worker.restartCount++;
1944
- this.workers.delete(addonId);
1945
- this.forkWorker(addonId, addonDir, config, storagePaths, dataDir, locationPaths, workerToken).catch((err) => {
1946
- console.error(`[WorkerHost] Failed to restart worker ${addonId}:`, err);
1769
+ this.spawn(config).catch(() => {
1947
1770
  });
1948
- }, backoff);
1949
- }
1950
- async shutdownWorker(addonId, worker) {
1951
- worker.state = "stopping";
1952
- worker.process.kill("SIGTERM");
1953
- await new Promise((resolve) => {
1954
- const timeout = setTimeout(() => {
1955
- console.warn(`[WorkerHost] Worker ${addonId} did not exit within ${this.options.shutdownTimeoutMs}ms \u2014 SIGKILL`);
1956
- worker.process.kill("SIGKILL");
1957
- resolve();
1958
- }, this.options.shutdownTimeoutMs);
1959
- worker.process.on("exit", () => {
1960
- clearTimeout(timeout);
1961
- resolve();
1962
- });
1963
- });
1771
+ }, 2e3);
1964
1772
  }
1773
+ });
1774
+ return managed;
1775
+ }
1776
+ listProcesses() {
1777
+ return [...this.processes.values()].map((p) => p.toInfo());
1778
+ }
1779
+ getWorkerStats() {
1780
+ const mem = process.memoryUsage();
1781
+ const cpu = process.cpuUsage();
1782
+ return {
1783
+ pid: process.pid,
1784
+ cpuPercent: (cpu.user + cpu.system) / 1e6,
1785
+ memoryRss: mem.rss,
1786
+ heapUsed: mem.heapUsed,
1787
+ uptimeSeconds: Math.round(process.uptime()),
1788
+ restartCount: 0,
1789
+ state: "running"
1965
1790
  };
1966
- exports2.AddonWorkerHost = AddonWorkerHost2;
1967
1791
  }
1968
- });
1969
-
1970
- // src/worker/worker-process-manager.js
1971
- var require_worker_process_manager = __commonJS({
1972
- "src/worker/worker-process-manager.js"(exports2) {
1973
- "use strict";
1974
- Object.defineProperty(exports2, "__esModule", { value: true });
1975
- exports2.WorkerProcessManager = void 0;
1976
- var node_child_process_1 = require("child_process");
1977
- var WorkerProcessManager2 = class {
1978
- sendToMain;
1979
- processes = /* @__PURE__ */ new Map();
1980
- constructor(sendToMain) {
1981
- this.sendToMain = sendToMain;
1982
- }
1983
- async spawn(config) {
1984
- const child = (0, node_child_process_1.spawn)(config.command, [...config.args ?? []], {
1985
- cwd: config.cwd,
1986
- env: config.env ? { ...process.env, ...config.env } : void 0,
1987
- stdio: ["pipe", "pipe", "pipe"]
1988
- });
1989
- const managed = new ManagedProcess(child, config, this.sendToMain);
1990
- this.processes.set(child.pid, managed);
1991
- this.sendToMain({
1992
- type: "SUB_PROCESS_SPAWNED",
1993
- pid: child.pid,
1994
- name: config.name,
1995
- command: config.command
1996
- });
1997
- child.on("exit", (code) => {
1998
- this.sendToMain({
1999
- type: "SUB_PROCESS_EXITED",
2000
- pid: child.pid,
2001
- code
2002
- });
2003
- if (config.autoRestart && managed.restartCount < (config.maxRestarts ?? 3)) {
2004
- managed.restartCount++;
2005
- setTimeout(() => {
2006
- this.spawn(config).catch(() => {
2007
- });
2008
- }, 2e3);
2009
- }
2010
- });
2011
- return managed;
2012
- }
2013
- listProcesses() {
2014
- return [...this.processes.values()].map((p) => p.toInfo());
2015
- }
2016
- getWorkerStats() {
2017
- const mem = process.memoryUsage();
2018
- const cpu = process.cpuUsage();
2019
- return {
2020
- pid: process.pid,
2021
- cpuPercent: (cpu.user + cpu.system) / 1e6,
2022
- memoryRss: mem.rss,
2023
- heapUsed: mem.heapUsed,
2024
- uptimeSeconds: Math.round(process.uptime()),
2025
- restartCount: 0,
2026
- state: "running"
2027
- };
2028
- }
2029
- async killAll() {
2030
- for (const [, proc] of this.processes) {
2031
- proc.kill("SIGTERM");
2032
- }
2033
- this.processes.clear();
2034
- }
1792
+ async killAll() {
1793
+ for (const [, proc] of this.processes) {
1794
+ proc.kill("SIGTERM");
1795
+ }
1796
+ this.processes.clear();
1797
+ }
1798
+ };
1799
+ var ManagedProcess = class {
1800
+ constructor(child, config, sendToMain) {
1801
+ this.child = child;
1802
+ this.config = config;
1803
+ this.sendToMain = sendToMain;
1804
+ this.pid = child.pid;
1805
+ this.name = config.name;
1806
+ child.on("exit", (code) => {
1807
+ for (const handler of this.exitHandlers) handler(code);
1808
+ });
1809
+ child.on("error", (err) => {
1810
+ for (const handler of this.errorHandlers) handler(err);
1811
+ });
1812
+ }
1813
+ pid;
1814
+ name;
1815
+ restartCount = 0;
1816
+ startedAt = Date.now();
1817
+ exitHandlers = [];
1818
+ errorHandlers = [];
1819
+ getStats() {
1820
+ return {
1821
+ pid: this.pid,
1822
+ cpuPercent: 0,
1823
+ memoryRss: 0,
1824
+ uptimeSeconds: Math.round((Date.now() - this.startedAt) / 1e3),
1825
+ restartCount: this.restartCount,
1826
+ state: this.child.exitCode !== null ? "stopped" : "running"
2035
1827
  };
2036
- exports2.WorkerProcessManager = WorkerProcessManager2;
2037
- var ManagedProcess = class {
2038
- child;
2039
- config;
2040
- sendToMain;
2041
- pid;
2042
- name;
2043
- restartCount = 0;
2044
- startedAt = Date.now();
2045
- exitHandlers = [];
2046
- errorHandlers = [];
2047
- constructor(child, config, sendToMain) {
2048
- this.child = child;
2049
- this.config = config;
2050
- this.sendToMain = sendToMain;
2051
- this.pid = child.pid;
2052
- this.name = config.name;
2053
- child.on("exit", (code) => {
2054
- for (const handler of this.exitHandlers)
2055
- handler(code);
2056
- });
2057
- child.on("error", (err) => {
2058
- for (const handler of this.errorHandlers)
2059
- handler(err);
2060
- });
2061
- }
2062
- getStats() {
2063
- return {
2064
- pid: this.pid,
2065
- cpuPercent: 0,
2066
- memoryRss: 0,
2067
- uptimeSeconds: Math.round((Date.now() - this.startedAt) / 1e3),
2068
- restartCount: this.restartCount,
2069
- state: this.child.exitCode !== null ? "stopped" : "running"
2070
- };
2071
- }
2072
- write(data) {
2073
- this.child.stdin?.write(data);
2074
- }
2075
- get stdout() {
2076
- return this.child.stdout;
2077
- }
2078
- get stderr() {
2079
- return this.child.stderr;
2080
- }
2081
- kill(signal = "SIGTERM") {
2082
- this.child.kill(signal);
2083
- }
2084
- wait() {
2085
- if (this.child.exitCode !== null) {
2086
- return Promise.resolve({ code: this.child.exitCode, signal: null });
2087
- }
2088
- return new Promise((resolve) => {
2089
- this.child.on("exit", (code, signal) => {
2090
- resolve({ code, signal });
2091
- });
2092
- });
2093
- }
2094
- onExit(handler) {
2095
- this.exitHandlers.push(handler);
2096
- }
2097
- onError(handler) {
2098
- this.errorHandlers.push(handler);
2099
- }
2100
- toInfo() {
2101
- return {
2102
- pid: this.pid,
2103
- name: this.name,
2104
- command: this.config.command,
2105
- state: this.child.exitCode !== null ? "stopped" : "running",
2106
- cpuPercent: 0,
2107
- memoryRss: 0,
2108
- uptimeSeconds: Math.round((Date.now() - this.startedAt) / 1e3)
2109
- };
2110
- }
1828
+ }
1829
+ write(data) {
1830
+ this.child.stdin?.write(data);
1831
+ }
1832
+ get stdout() {
1833
+ return this.child.stdout;
1834
+ }
1835
+ get stderr() {
1836
+ return this.child.stderr;
1837
+ }
1838
+ kill(signal = "SIGTERM") {
1839
+ this.child.kill(signal);
1840
+ }
1841
+ wait() {
1842
+ if (this.child.exitCode !== null) {
1843
+ return Promise.resolve({ code: this.child.exitCode, signal: null });
1844
+ }
1845
+ return new Promise((resolve3) => {
1846
+ this.child.on("exit", (code, signal) => {
1847
+ resolve3({ code, signal });
1848
+ });
1849
+ });
1850
+ }
1851
+ onExit(handler) {
1852
+ this.exitHandlers.push(handler);
1853
+ }
1854
+ onError(handler) {
1855
+ this.errorHandlers.push(handler);
1856
+ }
1857
+ toInfo() {
1858
+ return {
1859
+ pid: this.pid,
1860
+ name: this.name,
1861
+ command: this.config.command,
1862
+ state: this.child.exitCode !== null ? "stopped" : "running",
1863
+ cpuPercent: 0,
1864
+ memoryRss: 0,
1865
+ uptimeSeconds: Math.round((Date.now() - this.startedAt) / 1e3)
2111
1866
  };
2112
1867
  }
2113
- });
2114
-
2115
- // src/index.ts
2116
- var src_exports = {};
2117
- __export(src_exports, {
2118
- AddonEngineManager: () => import_addon_engine_manager.AddonEngineManager,
2119
- AddonInstaller: () => import_addon_installer.AddonInstaller,
2120
- AddonLoader: () => import_addon_loader.AddonLoader,
2121
- AddonWorkerHost: () => import_addon_worker_host.AddonWorkerHost,
2122
- CapabilityRegistry: () => import_capability_registry.CapabilityRegistry,
2123
- ConfigManager: () => import_config_manager.ConfigManager,
2124
- DEFAULT_DATA_PATH: () => import_config_schema.DEFAULT_DATA_PATH,
2125
- INFRA_CAPABILITIES: () => import_infra_capabilities.INFRA_CAPABILITIES,
2126
- RUNTIME_DEFAULTS: () => import_config_schema.RUNTIME_DEFAULTS,
2127
- WorkerProcessManager: () => import_worker_process_manager.WorkerProcessManager,
2128
- bootstrapSchema: () => import_config_schema.bootstrapSchema,
2129
- copyDirRecursive: () => import_fs_utils.copyDirRecursive,
2130
- copyExtraFileDirs: () => import_fs_utils.copyExtraFileDirs,
2131
- detectWorkspacePackagesDir: () => import_workspace_detect.detectWorkspacePackagesDir,
2132
- ensureDir: () => import_fs_utils.ensureDir,
2133
- ensureLibraryBuilt: () => import_fs_utils.ensureLibraryBuilt,
2134
- installPackageFromNpmSync: () => import_fs_utils.installPackageFromNpmSync,
2135
- isInfraCapability: () => import_infra_capabilities.isInfraCapability,
2136
- isSourceNewer: () => import_fs_utils.isSourceNewer,
2137
- stripCamstackDeps: () => import_fs_utils.stripCamstackDeps,
2138
- symlinkAddonsToNodeModules: () => import_fs_utils.symlinkAddonsToNodeModules
2139
- });
2140
- module.exports = __toCommonJS(src_exports);
2141
- var import_addon_loader = __toESM(require_addon_loader());
2142
- var import_addon_engine_manager = __toESM(require_addon_engine_manager());
2143
- var import_addon_installer = __toESM(require_addon_installer());
2144
- var import_fs_utils = __toESM(require_fs_utils());
2145
- var import_workspace_detect = __toESM(require_workspace_detect());
2146
- var import_capability_registry = __toESM(require_capability_registry());
2147
- var import_infra_capabilities = __toESM(require_infra_capabilities());
2148
- var import_config_manager = __toESM(require_config_manager());
2149
- var import_config_schema = __toESM(require_config_schema());
2150
- var import_addon_worker_host = __toESM(require_addon_worker_host());
2151
- var import_worker_process_manager = __toESM(require_worker_process_manager());
1868
+ };
2152
1869
  // Annotate the CommonJS export names for ESM import in node:
2153
1870
  0 && (module.exports = {
2154
1871
  AddonEngineManager,