@camstack/kernel 0.1.6 → 0.1.9

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.
@@ -0,0 +1,7 @@
1
+ import {
2
+ AddonInstaller
3
+ } from "./chunk-67QMERMP.mjs";
4
+ export {
5
+ AddonInstaller
6
+ };
7
+ //# sourceMappingURL=addon-installer-2BW2KLQD.mjs.map
@@ -0,0 +1,411 @@
1
+ // src/addon-installer.ts
2
+ import { execFile } from "child_process";
3
+ import { promisify } from "util";
4
+ import * as fs2 from "fs";
5
+ import * as path2 from "path";
6
+ import * as os2 from "os";
7
+
8
+ // src/fs-utils.ts
9
+ import { execFileSync } from "child_process";
10
+ import * as fs from "fs";
11
+ import * as os from "os";
12
+ import * as path from "path";
13
+ function ensureDir(dirPath) {
14
+ fs.mkdirSync(dirPath, { recursive: true });
15
+ }
16
+ function copyDirRecursive(src, dest) {
17
+ ensureDir(dest);
18
+ const entries = fs.readdirSync(src, { withFileTypes: true });
19
+ for (const entry of entries) {
20
+ const srcPath = path.join(src, entry.name);
21
+ const destPath = path.join(dest, entry.name);
22
+ if (entry.isDirectory()) {
23
+ copyDirRecursive(srcPath, destPath);
24
+ } else {
25
+ fs.copyFileSync(srcPath, destPath);
26
+ }
27
+ }
28
+ }
29
+ function stripCamstackDeps(pkg) {
30
+ const result = { ...pkg };
31
+ for (const depType of ["dependencies", "peerDependencies", "devDependencies"]) {
32
+ const deps = result[depType];
33
+ if (deps) {
34
+ const filtered = {};
35
+ for (const [name, version] of Object.entries(deps)) {
36
+ if (!name.startsWith("@camstack/")) {
37
+ filtered[name] = version;
38
+ }
39
+ }
40
+ result[depType] = Object.keys(filtered).length > 0 ? filtered : void 0;
41
+ }
42
+ }
43
+ delete result.devDependencies;
44
+ return result;
45
+ }
46
+ function copyExtraFileDirs(pkgJson, sourceDir, destDir) {
47
+ const files = pkgJson.files;
48
+ if (!files) return;
49
+ for (const fileEntry of files) {
50
+ if (fileEntry === "dist" || fileEntry.includes("*")) continue;
51
+ const srcPath = path.join(sourceDir, fileEntry);
52
+ if (!fs.existsSync(srcPath)) continue;
53
+ const destPath = path.join(destDir, fileEntry);
54
+ const stat = fs.statSync(srcPath);
55
+ if (stat.isDirectory()) {
56
+ copyDirRecursive(srcPath, destPath);
57
+ } else if (stat.isFile()) {
58
+ ensureDir(path.dirname(destPath));
59
+ fs.copyFileSync(srcPath, destPath);
60
+ }
61
+ }
62
+ }
63
+ function symlinkAddonsToNodeModules(addonsDir, nodeModulesDir) {
64
+ const camstackDir = path.join(nodeModulesDir, "@camstack");
65
+ ensureDir(camstackDir);
66
+ const packagesToLink = ["core"];
67
+ for (const pkg of packagesToLink) {
68
+ const addonDir = path.join(addonsDir, "@camstack", pkg);
69
+ const linkPath = path.join(camstackDir, pkg);
70
+ if (!fs.existsSync(addonDir)) continue;
71
+ try {
72
+ const stat = fs.lstatSync(linkPath);
73
+ if (stat.isSymbolicLink() || stat.isDirectory()) {
74
+ fs.rmSync(linkPath, { recursive: true, force: true });
75
+ }
76
+ } catch {
77
+ }
78
+ fs.symlinkSync(addonDir, linkPath, "dir");
79
+ console.log(`[symlink] node_modules/@camstack/${pkg} -> ${addonDir}`);
80
+ }
81
+ }
82
+ function isSourceNewer(packageDir) {
83
+ const srcDir = path.join(packageDir, "src");
84
+ const distDir = path.join(packageDir, "dist");
85
+ if (!fs.existsSync(srcDir) || !fs.existsSync(distDir)) return true;
86
+ try {
87
+ const distMtime = fs.statSync(distDir).mtimeMs;
88
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true, recursive: true });
89
+ for (const entry of entries) {
90
+ if (!entry.isFile()) continue;
91
+ const filePath = path.join(entry.parentPath ?? entry.path, entry.name);
92
+ if (fs.statSync(filePath).mtimeMs > distMtime) return true;
93
+ }
94
+ return false;
95
+ } catch {
96
+ return true;
97
+ }
98
+ }
99
+ function ensureLibraryBuilt(packageName, packagesDir) {
100
+ const dirName = packageName.replace("@camstack/", "");
101
+ const sourceDir = path.join(packagesDir, dirName);
102
+ if (!fs.existsSync(sourceDir)) return;
103
+ const distDir = path.join(sourceDir, "dist");
104
+ const hasIndex = fs.existsSync(path.join(distDir, "index.js")) || fs.existsSync(path.join(distDir, "index.mjs"));
105
+ if (hasIndex) return;
106
+ console.warn(`[ensureLibraryBuilt] ${packageName} has no dist/ \u2014 run 'npm run build' first`);
107
+ }
108
+ function installPackageFromNpmSync(packageName, targetDir) {
109
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "camstack-install-"));
110
+ try {
111
+ const stdout = execFileSync("npm", ["pack", packageName, "--pack-destination", tmpDir], {
112
+ timeout: 12e4,
113
+ encoding: "utf-8"
114
+ });
115
+ const tgzFilename = stdout.trim().split("\n").pop()?.trim();
116
+ if (!tgzFilename) throw new Error("npm pack produced no output");
117
+ const tgzPath = path.join(tmpDir, tgzFilename);
118
+ const extractDir = path.join(tmpDir, "extracted");
119
+ ensureDir(extractDir);
120
+ execFileSync("tar", ["-xzf", tgzPath, "-C", extractDir], { timeout: 3e4 });
121
+ const packageSubDir = path.join(extractDir, "package");
122
+ const srcPkgJsonDir = fs.existsSync(path.join(packageSubDir, "package.json")) ? packageSubDir : extractDir;
123
+ fs.rmSync(targetDir, { recursive: true, force: true });
124
+ ensureDir(targetDir);
125
+ fs.copyFileSync(path.join(srcPkgJsonDir, "package.json"), path.join(targetDir, "package.json"));
126
+ const distSrc = path.join(srcPkgJsonDir, "dist");
127
+ if (fs.existsSync(distSrc)) {
128
+ copyDirRecursive(distSrc, path.join(targetDir, "dist"));
129
+ }
130
+ try {
131
+ const npmPkg = JSON.parse(fs.readFileSync(path.join(srcPkgJsonDir, "package.json"), "utf-8"));
132
+ copyExtraFileDirs(npmPkg, srcPkgJsonDir, targetDir);
133
+ } catch {
134
+ }
135
+ } finally {
136
+ fs.rmSync(tmpDir, { recursive: true, force: true });
137
+ }
138
+ }
139
+
140
+ // src/addon-installer.ts
141
+ var execFileAsync = promisify(execFile);
142
+ var AddonInstaller = class _AddonInstaller {
143
+ addonsDir;
144
+ registry;
145
+ workspacePackagesDir;
146
+ constructor(config) {
147
+ this.addonsDir = config.addonsDir;
148
+ this.registry = config.registry;
149
+ this.workspacePackagesDir = config.workspacePackagesDir;
150
+ }
151
+ /** Required addon packages that must be installed for the server to function */
152
+ static REQUIRED_PACKAGES = [
153
+ "@camstack/core",
154
+ "@camstack/addon-stream-broker",
155
+ "@camstack/addon-recording",
156
+ "@camstack/addon-vision",
157
+ "@camstack/addon-admin-ui",
158
+ "@camstack/addon-webrtc-adaptive",
159
+ "@camstack/addon-analytics",
160
+ "@camstack/addon-scene-intelligence",
161
+ "@camstack/addon-advanced-notifier"
162
+ ];
163
+ /** Ensure the addons directory exists */
164
+ async initialize() {
165
+ ensureDir(this.addonsDir);
166
+ }
167
+ /**
168
+ * Ensure all required packages are installed in the addons directory.
169
+ * This replaces the standalone first-boot-installer.ts.
170
+ */
171
+ async ensureRequiredPackages() {
172
+ ensureDir(this.addonsDir);
173
+ if (this.workspacePackagesDir) {
174
+ console.log(`[AddonInstaller] Workspace detected: ${this.workspacePackagesDir}`);
175
+ ensureLibraryBuilt("@camstack/kernel", this.workspacePackagesDir);
176
+ ensureLibraryBuilt("@camstack/types", this.workspacePackagesDir);
177
+ }
178
+ for (const packageName of _AddonInstaller.REQUIRED_PACKAGES) {
179
+ const addonDir = path2.join(this.addonsDir, packageName);
180
+ const pkgJsonPath = path2.join(addonDir, "package.json");
181
+ const forceNpm = process.env["CAMSTACK_INSTALL_SOURCE"] === "npm";
182
+ const useWorkspace = this.workspacePackagesDir && !forceNpm;
183
+ if (fs2.existsSync(pkgJsonPath)) {
184
+ if (!useWorkspace) {
185
+ console.log(`[AddonInstaller] ${packageName} \u2014 already installed (npm), skipping`);
186
+ continue;
187
+ }
188
+ const srcPkgDir = this.findWorkspacePackage(packageName);
189
+ if (srcPkgDir && !isSourceNewer(srcPkgDir)) {
190
+ console.log(`[AddonInstaller] ${packageName} \u2014 workspace up-to-date, skipping`);
191
+ continue;
192
+ }
193
+ }
194
+ try {
195
+ if (useWorkspace) {
196
+ const pkgDir = this.findWorkspacePackage(packageName);
197
+ if (pkgDir) {
198
+ console.log(`[AddonInstaller] ${packageName} \u2014 installing from workspace`);
199
+ await this.installFromWorkspace(packageName);
200
+ continue;
201
+ }
202
+ }
203
+ console.log(`[AddonInstaller] ${packageName} \u2014 installing from npm`);
204
+ await this.installFromNpm(packageName);
205
+ } catch (err) {
206
+ const msg = err instanceof Error ? err.message : String(err);
207
+ if (packageName === "@camstack/core") {
208
+ throw new Error(`Required package ${packageName} failed to install: ${msg}`);
209
+ }
210
+ console.error(`[AddonInstaller] Failed to install ${packageName}: ${msg}`);
211
+ }
212
+ }
213
+ }
214
+ findWorkspacePackage(packageName) {
215
+ if (!this.workspacePackagesDir) return null;
216
+ const shortName = packageName.replace("@camstack/", "");
217
+ for (const dirName of [shortName, `addon-${shortName}`, shortName.replace("addon-", "")]) {
218
+ const candidate = path2.join(this.workspacePackagesDir, dirName);
219
+ if (fs2.existsSync(path2.join(candidate, "package.json"))) {
220
+ try {
221
+ const pkg = JSON.parse(fs2.readFileSync(path2.join(candidate, "package.json"), "utf-8"));
222
+ if (pkg.name === packageName) return candidate;
223
+ } catch {
224
+ }
225
+ }
226
+ }
227
+ return null;
228
+ }
229
+ /** Install addon from a tgz file (uploaded or downloaded) */
230
+ async installFromTgz(tgzPath) {
231
+ const tmpDir = fs2.mkdtempSync(path2.join(os2.tmpdir(), "camstack-addon-install-"));
232
+ try {
233
+ await execFileAsync("tar", ["-xzf", tgzPath, "-C", tmpDir], { timeout: 3e4 });
234
+ const extractedDir = path2.join(tmpDir, "package");
235
+ const pkgJsonPath = fs2.existsSync(path2.join(extractedDir, "package.json")) ? path2.join(extractedDir, "package.json") : path2.join(tmpDir, "package.json");
236
+ if (!fs2.existsSync(pkgJsonPath)) {
237
+ throw new Error("No package.json found in tgz archive");
238
+ }
239
+ const pkgJson = JSON.parse(fs2.readFileSync(pkgJsonPath, "utf-8"));
240
+ if (!pkgJson.camstack?.addons) {
241
+ throw new Error(`Package ${pkgJson.name} has no camstack.addons manifest`);
242
+ }
243
+ const targetDir = path2.join(this.addonsDir, pkgJson.name);
244
+ fs2.rmSync(targetDir, { recursive: true, force: true });
245
+ ensureDir(targetDir);
246
+ const sourceDir = path2.dirname(pkgJsonPath);
247
+ fs2.copyFileSync(pkgJsonPath, path2.join(targetDir, "package.json"));
248
+ const sourceDist = path2.join(sourceDir, "dist");
249
+ if (fs2.existsSync(sourceDist)) {
250
+ copyDirRecursive(sourceDist, path2.join(targetDir, "dist"));
251
+ }
252
+ copyExtraFileDirs(pkgJson, sourceDir, targetDir);
253
+ return { name: pkgJson.name, version: pkgJson.version };
254
+ } finally {
255
+ fs2.rmSync(tmpDir, { recursive: true, force: true });
256
+ }
257
+ }
258
+ /**
259
+ * Install addon — prefers workspace if available, falls back to npm.
260
+ * This ensures dev builds (with vite output, etc.) are used when in workspace mode.
261
+ */
262
+ async install(packageName, version) {
263
+ if (this.workspacePackagesDir) {
264
+ const workspaceDirName = packageName.replace("@camstack/", "");
265
+ const sourceDir = path2.join(this.workspacePackagesDir, workspaceDirName);
266
+ if (fs2.existsSync(path2.join(sourceDir, "package.json"))) {
267
+ return this.installFromWorkspace(packageName);
268
+ }
269
+ }
270
+ return this.installFromNpm(packageName, version);
271
+ }
272
+ /**
273
+ * Install addon from workspace by copying package.json + dist/ to addons dir.
274
+ * Does NOT build — expects dist/ to already exist (run `npm run build` separately).
275
+ */
276
+ async installFromWorkspace(packageName) {
277
+ if (!this.workspacePackagesDir) {
278
+ throw new Error("Workspace packages directory not configured");
279
+ }
280
+ const workspaceDirName = packageName.replace("@camstack/", "");
281
+ const sourceDir = path2.join(this.workspacePackagesDir, workspaceDirName);
282
+ const sourcePkgJson = path2.join(sourceDir, "package.json");
283
+ if (!fs2.existsSync(sourcePkgJson)) {
284
+ throw new Error(`Workspace package not found: ${sourceDir}`);
285
+ }
286
+ const distDir = path2.join(sourceDir, "dist");
287
+ if (!fs2.existsSync(distDir)) {
288
+ throw new Error(`${packageName} has no dist/ directory \u2014 run 'npm run build' first`);
289
+ }
290
+ const targetDir = path2.join(this.addonsDir, packageName);
291
+ fs2.rmSync(targetDir, { recursive: true, force: true });
292
+ ensureDir(targetDir);
293
+ const pkgData = JSON.parse(fs2.readFileSync(sourcePkgJson, "utf-8"));
294
+ const strippedPkg = stripCamstackDeps(pkgData);
295
+ fs2.writeFileSync(path2.join(targetDir, "package.json"), JSON.stringify(strippedPkg, null, 2));
296
+ copyDirRecursive(distDir, path2.join(targetDir, "dist"));
297
+ copyExtraFileDirs(pkgData, sourceDir, targetDir);
298
+ fs2.writeFileSync(path2.join(targetDir, ".install-source"), "workspace");
299
+ try {
300
+ await execFileAsync("npm", ["install", "--omit=dev", "--ignore-scripts=false"], {
301
+ cwd: targetDir,
302
+ timeout: 12e4
303
+ });
304
+ } catch {
305
+ }
306
+ return { name: pkgData.name, version: pkgData.version };
307
+ }
308
+ /** Install addon from npm (download tgz, then extract) */
309
+ async installFromNpm(packageName, version) {
310
+ const tmpDir = fs2.mkdtempSync(path2.join(os2.tmpdir(), "camstack-addon-npm-"));
311
+ const packageSpec = version ? `${packageName}@${version}` : packageName;
312
+ const args = ["pack", packageSpec, "--pack-destination", tmpDir];
313
+ if (this.registry) {
314
+ args.push("--registry", this.registry);
315
+ }
316
+ console.log(`[AddonInstaller] npm pack ${packageSpec} \u2192 ${tmpDir}`);
317
+ try {
318
+ const { stdout } = await execFileAsync("npm", args, {
319
+ timeout: 12e4
320
+ });
321
+ const tgzFiles = fs2.readdirSync(tmpDir).filter((f) => f.endsWith(".tgz"));
322
+ console.log(`[AddonInstaller] npm pack stdout: ${stdout.trim()}, tgzFiles: ${tgzFiles.join(", ")}`);
323
+ if (tgzFiles.length === 0) {
324
+ throw new Error(`npm pack produced no tgz file for ${packageSpec}. stdout: ${stdout.trim()}`);
325
+ }
326
+ const tgzPath = path2.join(tmpDir, tgzFiles[0]);
327
+ const result = await this.installFromTgz(tgzPath);
328
+ console.log(`[AddonInstaller] installFromTgz result: ${result.name}@${result.version} \u2192 ${path2.join(this.addonsDir, result.name)}`);
329
+ const targetDir = path2.join(this.addonsDir, result.name);
330
+ fs2.writeFileSync(path2.join(targetDir, ".install-source"), "npm");
331
+ return result;
332
+ } finally {
333
+ fs2.rmSync(tmpDir, { recursive: true, force: true });
334
+ }
335
+ }
336
+ /** Uninstall addon (delete directory) */
337
+ async uninstall(packageName) {
338
+ const addonDir = path2.join(this.addonsDir, packageName);
339
+ if (fs2.existsSync(addonDir)) {
340
+ fs2.rmSync(addonDir, { recursive: true, force: true });
341
+ return;
342
+ }
343
+ const legacyDir = path2.join(this.addonsDir, packageName.replace(/^@[^/]+\//, ""));
344
+ if (fs2.existsSync(legacyDir)) {
345
+ fs2.rmSync(legacyDir, { recursive: true, force: true });
346
+ }
347
+ }
348
+ /** List installed addons (directories with package.json containing camstack.addons) */
349
+ listInstalled() {
350
+ if (!fs2.existsSync(this.addonsDir)) return [];
351
+ const packageDirs = [];
352
+ for (const entry of fs2.readdirSync(this.addonsDir, { withFileTypes: true })) {
353
+ if (!entry.isDirectory()) continue;
354
+ if (entry.name.startsWith("@")) {
355
+ const scopeDir = path2.join(this.addonsDir, entry.name);
356
+ for (const inner of fs2.readdirSync(scopeDir, { withFileTypes: true })) {
357
+ if (inner.isDirectory()) packageDirs.push(path2.join(scopeDir, inner.name));
358
+ }
359
+ } else {
360
+ packageDirs.push(path2.join(this.addonsDir, entry.name));
361
+ }
362
+ }
363
+ return packageDirs.map((dir) => {
364
+ const pkgPath = path2.join(dir, "package.json");
365
+ if (!fs2.existsSync(pkgPath)) return null;
366
+ try {
367
+ const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
368
+ if (!pkg.camstack?.addons) return null;
369
+ const sourceFile = path2.join(dir, ".install-source");
370
+ const installSource = fs2.existsSync(sourceFile) ? fs2.readFileSync(sourceFile, "utf-8").trim() : void 0;
371
+ const result = { name: pkg.name, version: pkg.version, dir, ...installSource ? { installSource } : {} };
372
+ return result;
373
+ } catch {
374
+ return null;
375
+ }
376
+ }).filter((item) => item !== null);
377
+ }
378
+ /** Check if an addon is installed */
379
+ isInstalled(packageName) {
380
+ if (fs2.existsSync(path2.join(this.addonsDir, packageName, "package.json"))) return true;
381
+ const legacy = packageName.replace(/^@[^/]+\//, "");
382
+ return fs2.existsSync(path2.join(this.addonsDir, legacy, "package.json"));
383
+ }
384
+ /** Get installed package info */
385
+ getInstalledPackage(packageName) {
386
+ let pkgPath = path2.join(this.addonsDir, packageName, "package.json");
387
+ if (!fs2.existsSync(pkgPath)) {
388
+ pkgPath = path2.join(this.addonsDir, packageName.replace(/^@[^/]+\//, ""), "package.json");
389
+ }
390
+ if (!fs2.existsSync(pkgPath)) return null;
391
+ try {
392
+ const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
393
+ return { name: pkg.name, version: pkg.version, dir: path2.dirname(pkgPath) };
394
+ } catch {
395
+ return null;
396
+ }
397
+ }
398
+ };
399
+
400
+ export {
401
+ ensureDir,
402
+ copyDirRecursive,
403
+ stripCamstackDeps,
404
+ copyExtraFileDirs,
405
+ symlinkAddonsToNodeModules,
406
+ isSourceNewer,
407
+ ensureLibraryBuilt,
408
+ installPackageFromNpmSync,
409
+ AddonInstaller
410
+ };
411
+ //# sourceMappingURL=chunk-67QMERMP.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/addon-installer.ts","../src/fs-utils.ts"],"sourcesContent":["import { execFile } from 'node:child_process'\nimport { promisify } from 'node:util'\nimport * as fs from 'node:fs'\nimport * as path from 'node:path'\nimport * as os from 'node:os'\nimport { copyDirRecursive, copyExtraFileDirs, ensureDir, ensureLibraryBuilt, isSourceNewer, stripCamstackDeps } from './fs-utils.js'\n\nconst execFileAsync = promisify(execFile)\n\nexport interface AddonInstallerConfig {\n /** Directory where addons are stored (e.g., {dataDir}/addons) — each addon is a subdirectory */\n readonly addonsDir: string\n /** npm registry URL (default: https://registry.npmjs.org) */\n readonly registry?: string\n /** Workspace packages directory (e.g., /path/to/camstack-server/packages) — for dev installs */\n readonly workspacePackagesDir?: string\n}\n\nimport type { InstalledPackage } from '@camstack/types'\nexport type { InstalledPackage }\n\nexport class AddonInstaller {\n private readonly addonsDir: string\n private readonly registry: string | undefined\n private readonly workspacePackagesDir: string | undefined\n\n constructor(config: AddonInstallerConfig) {\n this.addonsDir = config.addonsDir\n this.registry = config.registry\n this.workspacePackagesDir = config.workspacePackagesDir\n }\n\n /** Required addon packages that must be installed for the server to function */\n static readonly REQUIRED_PACKAGES = [\n '@camstack/core',\n '@camstack/addon-stream-broker',\n '@camstack/addon-recording',\n '@camstack/addon-vision',\n '@camstack/addon-admin-ui',\n '@camstack/addon-webrtc-adaptive',\n '@camstack/addon-analytics',\n '@camstack/addon-scene-intelligence',\n '@camstack/addon-advanced-notifier',\n ] as const\n\n /** Ensure the addons directory exists */\n async initialize(): Promise<void> {\n ensureDir(this.addonsDir)\n }\n\n /**\n * Ensure all required packages are installed in the addons directory.\n * This replaces the standalone first-boot-installer.ts.\n */\n async ensureRequiredPackages(): Promise<void> {\n ensureDir(this.addonsDir)\n\n // In workspace mode, ensure library dependencies are built first\n if (this.workspacePackagesDir) {\n console.log(`[AddonInstaller] Workspace detected: ${this.workspacePackagesDir}`)\n ensureLibraryBuilt('@camstack/kernel', this.workspacePackagesDir)\n ensureLibraryBuilt('@camstack/types', this.workspacePackagesDir)\n }\n\n for (const packageName of AddonInstaller.REQUIRED_PACKAGES) {\n const addonDir = path.join(this.addonsDir, packageName)\n const pkgJsonPath = path.join(addonDir, 'package.json')\n\n // CAMSTACK_INSTALL_SOURCE=npm forces npm install even in workspace mode\n const forceNpm = process.env['CAMSTACK_INSTALL_SOURCE'] === 'npm'\n const useWorkspace = this.workspacePackagesDir && !forceNpm\n\n // Skip if already installed and up-to-date\n if (fs.existsSync(pkgJsonPath)) {\n if (!useWorkspace) {\n // Production or npm-forced: installed = done\n console.log(`[AddonInstaller] ${packageName} — already installed (npm), skipping`)\n continue\n }\n // Workspace: skip if source dist/ isn't newer than target\n const srcPkgDir = this.findWorkspacePackage(packageName)\n if (srcPkgDir && !isSourceNewer(srcPkgDir)) {\n console.log(`[AddonInstaller] ${packageName} — workspace up-to-date, skipping`)\n continue\n }\n }\n\n try {\n // Try workspace install first (dev mode), fall back to npm\n if (useWorkspace) {\n const pkgDir = this.findWorkspacePackage(packageName)\n if (pkgDir) {\n console.log(`[AddonInstaller] ${packageName} — installing from workspace`)\n await this.installFromWorkspace(packageName)\n continue\n }\n }\n\n console.log(`[AddonInstaller] ${packageName} — installing from npm`)\n await this.installFromNpm(packageName)\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err)\n // Core is essential — abort if it fails\n if (packageName === '@camstack/core') {\n throw new Error(`Required package ${packageName} failed to install: ${msg}`)\n }\n console.error(`[AddonInstaller] Failed to install ${packageName}: ${msg}`)\n }\n }\n }\n\n private findWorkspacePackage(packageName: string): string | null {\n if (!this.workspacePackagesDir) return null\n const shortName = packageName.replace('@camstack/', '')\n // Check common naming patterns\n for (const dirName of [shortName, `addon-${shortName}`, shortName.replace('addon-', '')]) {\n const candidate = path.join(this.workspacePackagesDir, dirName)\n if (fs.existsSync(path.join(candidate, 'package.json'))) {\n try {\n const pkg = JSON.parse(fs.readFileSync(path.join(candidate, 'package.json'), 'utf-8'))\n if (pkg.name === packageName) return candidate\n } catch { /* ignore */ }\n }\n }\n return null\n }\n\n /** Install addon from a tgz file (uploaded or downloaded) */\n async installFromTgz(tgzPath: string): Promise<{ name: string; version: string }> {\n // 1. Extract tgz to temp dir\n const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'camstack-addon-install-'))\n\n try {\n await execFileAsync('tar', ['-xzf', tgzPath, '-C', tmpDir], { timeout: 30_000 })\n\n // npm pack creates a package/ directory inside the tgz\n const extractedDir = path.join(tmpDir, 'package')\n const pkgJsonPath = fs.existsSync(path.join(extractedDir, 'package.json'))\n ? path.join(extractedDir, 'package.json')\n : path.join(tmpDir, 'package.json')\n\n if (!fs.existsSync(pkgJsonPath)) {\n throw new Error('No package.json found in tgz archive')\n }\n\n const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')) as {\n name: string\n version: string\n files?: string[]\n camstack?: { addons?: unknown[] }\n }\n\n if (!pkgJson.camstack?.addons) {\n throw new Error(`Package ${pkgJson.name} has no camstack.addons manifest`)\n }\n\n // 2. Determine target directory name (strip scope)\n const targetDir = path.join(this.addonsDir, pkgJson.name)\n\n // 3. Remove old version if present\n fs.rmSync(targetDir, { recursive: true, force: true })\n ensureDir(targetDir)\n\n // 4. Copy package.json\n const sourceDir = path.dirname(pkgJsonPath)\n fs.copyFileSync(pkgJsonPath, path.join(targetDir, 'package.json'))\n\n // 5. Copy dist/ directory\n const sourceDist = path.join(sourceDir, 'dist')\n if (fs.existsSync(sourceDist)) {\n copyDirRecursive(sourceDist, path.join(targetDir, 'dist'))\n }\n\n // 6. Copy extra directories from \"files\" in package.json (e.g., python/, reference-images/)\n copyExtraFileDirs(pkgJson, sourceDir, targetDir)\n\n return { name: pkgJson.name, version: pkgJson.version }\n } finally {\n fs.rmSync(tmpDir, { recursive: true, force: true })\n }\n }\n\n /**\n * Install addon — prefers workspace if available, falls back to npm.\n * This ensures dev builds (with vite output, etc.) are used when in workspace mode.\n */\n async install(packageName: string, version?: string): Promise<{ name: string; version: string }> {\n if (this.workspacePackagesDir) {\n // Workspace dirs use short names (e.g., packages/addon-benchmark)\n const workspaceDirName = packageName.replace('@camstack/', '')\n const sourceDir = path.join(this.workspacePackagesDir, workspaceDirName)\n if (fs.existsSync(path.join(sourceDir, 'package.json'))) {\n return this.installFromWorkspace(packageName)\n }\n }\n return this.installFromNpm(packageName, version)\n }\n\n /**\n * Install addon from workspace by copying package.json + dist/ to addons dir.\n * Does NOT build — expects dist/ to already exist (run `npm run build` separately).\n */\n async installFromWorkspace(packageName: string): Promise<{ name: string; version: string }> {\n if (!this.workspacePackagesDir) {\n throw new Error('Workspace packages directory not configured')\n }\n\n const workspaceDirName = packageName.replace('@camstack/', '')\n const sourceDir = path.join(this.workspacePackagesDir, workspaceDirName)\n const sourcePkgJson = path.join(sourceDir, 'package.json')\n\n if (!fs.existsSync(sourcePkgJson)) {\n throw new Error(`Workspace package not found: ${sourceDir}`)\n }\n\n const distDir = path.join(sourceDir, 'dist')\n if (!fs.existsSync(distDir)) {\n throw new Error(`${packageName} has no dist/ directory — run 'npm run build' first`)\n }\n\n // Copy package.json + dist/ to target (use full scoped name as dir)\n const targetDir = path.join(this.addonsDir, packageName)\n\n fs.rmSync(targetDir, { recursive: true, force: true })\n ensureDir(targetDir)\n\n // Strip @camstack/* deps from package.json\n const pkgData = JSON.parse(fs.readFileSync(sourcePkgJson, 'utf-8'))\n const strippedPkg = stripCamstackDeps(pkgData)\n fs.writeFileSync(path.join(targetDir, 'package.json'), JSON.stringify(strippedPkg, null, 2))\n\n copyDirRecursive(distDir, path.join(targetDir, 'dist'))\n\n // Copy extra directories from \"files\" in package.json (e.g., python/, reference-images/)\n copyExtraFileDirs(pkgData, sourceDir, targetDir)\n\n // Mark install source for UI display\n fs.writeFileSync(path.join(targetDir, '.install-source'), 'workspace')\n\n // Install production dependencies (non-fatal if fails)\n try {\n await execFileAsync('npm', ['install', '--omit=dev', '--ignore-scripts=false'], {\n cwd: targetDir,\n timeout: 120_000,\n })\n } catch {\n // Non-fatal — addon may not have external deps\n }\n\n return { name: pkgData.name, version: pkgData.version }\n }\n\n /** Install addon from npm (download tgz, then extract) */\n async installFromNpm(packageName: string, version?: string): Promise<{ name: string; version: string }> {\n const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'camstack-addon-npm-'))\n\n const packageSpec = version ? `${packageName}@${version}` : packageName\n const args = ['pack', packageSpec, '--pack-destination', tmpDir]\n if (this.registry) {\n args.push('--registry', this.registry)\n }\n\n console.log(`[AddonInstaller] npm pack ${packageSpec} → ${tmpDir}`)\n\n try {\n const { stdout } = await execFileAsync('npm', args, {\n timeout: 120_000,\n })\n\n const tgzFiles = fs.readdirSync(tmpDir).filter(f => f.endsWith('.tgz'))\n console.log(`[AddonInstaller] npm pack stdout: ${stdout.trim()}, tgzFiles: ${tgzFiles.join(', ')}`)\n if (tgzFiles.length === 0) {\n throw new Error(`npm pack produced no tgz file for ${packageSpec}. stdout: ${stdout.trim()}`)\n }\n\n const tgzPath = path.join(tmpDir, tgzFiles[0]!)\n const result = await this.installFromTgz(tgzPath)\n console.log(`[AddonInstaller] installFromTgz result: ${result.name}@${result.version} → ${path.join(this.addonsDir, result.name)}`)\n\n // Mark as npm install\n const targetDir = path.join(this.addonsDir, result.name)\n fs.writeFileSync(path.join(targetDir, '.install-source'), 'npm')\n\n return result\n } finally {\n // Clean up AFTER installFromTgz has fully completed\n fs.rmSync(tmpDir, { recursive: true, force: true })\n }\n }\n\n /** Uninstall addon (delete directory) */\n async uninstall(packageName: string): Promise<void> {\n // Support both full scoped name and short name\n const addonDir = path.join(this.addonsDir, packageName)\n if (fs.existsSync(addonDir)) {\n fs.rmSync(addonDir, { recursive: true, force: true })\n return\n }\n // Fallback: try without scope (legacy layout)\n const legacyDir = path.join(this.addonsDir, packageName.replace(/^@[^/]+\\//, ''))\n if (fs.existsSync(legacyDir)) {\n fs.rmSync(legacyDir, { recursive: true, force: true })\n }\n }\n\n /** List installed addons (directories with package.json containing camstack.addons) */\n listInstalled(): InstalledPackage[] {\n if (!fs.existsSync(this.addonsDir)) return []\n\n // Collect all package directories — handles both flat (addon-x/) and scoped (@camstack/addon-x/) layouts\n const packageDirs: string[] = []\n for (const entry of fs.readdirSync(this.addonsDir, { withFileTypes: true })) {\n if (!entry.isDirectory()) continue\n if (entry.name.startsWith('@')) {\n // Scoped directory — scan one level deeper\n const scopeDir = path.join(this.addonsDir, entry.name)\n for (const inner of fs.readdirSync(scopeDir, { withFileTypes: true })) {\n if (inner.isDirectory()) packageDirs.push(path.join(scopeDir, inner.name))\n }\n } else {\n packageDirs.push(path.join(this.addonsDir, entry.name))\n }\n }\n\n return packageDirs\n .map(dir => {\n const pkgPath = path.join(dir, 'package.json')\n if (!fs.existsSync(pkgPath)) return null\n try {\n const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as {\n name: string\n version: string\n camstack?: { addons?: unknown[] }\n }\n if (!pkg.camstack?.addons) return null\n const sourceFile = path.join(dir, '.install-source')\n const installSource = fs.existsSync(sourceFile)\n ? fs.readFileSync(sourceFile, 'utf-8').trim() as 'npm' | 'workspace' | 'upload'\n : undefined\n const result: InstalledPackage = { name: pkg.name, version: pkg.version, dir, ...(installSource ? { installSource } : {}) }\n return result\n } catch {\n return null\n }\n })\n .filter((item): item is InstalledPackage => item !== null)\n }\n\n /** Check if an addon is installed */\n isInstalled(packageName: string): boolean {\n // Check scoped path first, then legacy\n if (fs.existsSync(path.join(this.addonsDir, packageName, 'package.json'))) return true\n const legacy = packageName.replace(/^@[^/]+\\//, '')\n return fs.existsSync(path.join(this.addonsDir, legacy, 'package.json'))\n }\n\n /** Get installed package info */\n getInstalledPackage(packageName: string): InstalledPackage | null {\n let pkgPath = path.join(this.addonsDir, packageName, 'package.json')\n if (!fs.existsSync(pkgPath)) {\n pkgPath = path.join(this.addonsDir, packageName.replace(/^@[^/]+\\//, ''), 'package.json')\n }\n if (!fs.existsSync(pkgPath)) return null\n try {\n const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as { name: string; version: string }\n return { name: pkg.name, version: pkg.version, dir: path.dirname(pkgPath) }\n } catch {\n return null\n }\n }\n}\n\n// copyDirRecursive, copyExtraFileDirs, ensureDir, stripCamstackDeps\n// are now imported from ./fs-utils.js (single source of truth)\n","import { execFileSync } from 'node:child_process'\nimport * as fs from 'node:fs'\nimport * as os from 'node:os'\nimport * as path from 'node:path'\n\n/**\n * Ensure a directory exists (recursive).\n * Single source of truth — replaces scattered mkdirSync calls.\n */\nexport function ensureDir(dirPath: string): void {\n fs.mkdirSync(dirPath, { recursive: true })\n}\n\n/**\n * Copy a directory recursively.\n * Single source of truth — extracted from addon-installer + first-boot-installer.\n */\nexport function copyDirRecursive(src: string, dest: string): void {\n ensureDir(dest)\n const entries = fs.readdirSync(src, { withFileTypes: true })\n for (const entry of entries) {\n const srcPath = path.join(src, entry.name)\n const destPath = path.join(dest, entry.name)\n if (entry.isDirectory()) {\n copyDirRecursive(srcPath, destPath)\n } else {\n fs.copyFileSync(srcPath, destPath)\n }\n }\n}\n\n/**\n * Strip @camstack/* dependencies and devDependencies from a package.json object.\n * Used when installing addons into the addons directory — @camstack packages\n * are provided by the host runtime, not installed per-addon.\n *\n * Returns a new object (immutable).\n */\nexport function stripCamstackDeps(\n pkg: Record<string, unknown>,\n): Record<string, unknown> {\n const result = { ...pkg }\n\n for (const depType of ['dependencies', 'peerDependencies', 'devDependencies']) {\n const deps = result[depType] as Record<string, string> | undefined\n if (deps) {\n const filtered: Record<string, string> = {}\n for (const [name, version] of Object.entries(deps)) {\n if (!name.startsWith('@camstack/')) {\n filtered[name] = version\n }\n }\n result[depType] = Object.keys(filtered).length > 0 ? filtered : undefined\n }\n }\n\n // Always strip devDependencies — addons don't need them at runtime\n delete result.devDependencies\n\n return result\n}\n\n/**\n * Copy extra file directories declared in package.json \"files\" field.\n * Copies directories (not individual files) from source to destination.\n * Skips \"dist\" (already handled) and glob patterns.\n */\nexport function copyExtraFileDirs(\n pkgJson: Record<string, unknown>,\n sourceDir: string,\n destDir: string,\n): void {\n const files = pkgJson.files as string[] | undefined\n if (!files) return\n\n for (const fileEntry of files) {\n if (fileEntry === 'dist' || fileEntry.includes('*')) continue\n const srcPath = path.join(sourceDir, fileEntry)\n if (!fs.existsSync(srcPath)) continue\n\n const destPath = path.join(destDir, fileEntry)\n const stat = fs.statSync(srcPath)\n if (stat.isDirectory()) {\n copyDirRecursive(srcPath, destPath)\n } else if (stat.isFile()) {\n ensureDir(path.dirname(destPath))\n fs.copyFileSync(srcPath, destPath)\n }\n }\n}\n\n/**\n * Create symlinks in node_modules so that static `import from '@camstack/core'` works.\n * Links: node_modules/@camstack/{pkg} -> data/addons/{pkg}/\n * (not just dist -- need package.json for Node's exports resolution)\n */\nexport function symlinkAddonsToNodeModules(addonsDir: string, nodeModulesDir: string): void {\n const camstackDir = path.join(nodeModulesDir, '@camstack')\n ensureDir(camstackDir)\n\n // Link each addon that's in data/addons/ and also imported by the server\n const packagesToLink = ['core']\n\n for (const pkg of packagesToLink) {\n const addonDir = path.join(addonsDir, '@camstack', pkg)\n const linkPath = path.join(camstackDir, pkg)\n\n if (!fs.existsSync(addonDir)) continue\n\n // Remove existing link/directory\n try {\n const stat = fs.lstatSync(linkPath)\n if (stat.isSymbolicLink() || stat.isDirectory()) {\n fs.rmSync(linkPath, { recursive: true, force: true })\n }\n } catch {\n // Doesn't exist yet -- nothing to remove\n }\n\n fs.symlinkSync(addonDir, linkPath, 'dir')\n console.log(`[symlink] node_modules/@camstack/${pkg} -> ${addonDir}`)\n }\n}\n\n/**\n * Check if any file in src/ is newer than dist/.\n * Returns true if rebuild is needed.\n */\nexport function isSourceNewer(packageDir: string): boolean {\n const srcDir = path.join(packageDir, 'src')\n const distDir = path.join(packageDir, 'dist')\n if (!fs.existsSync(srcDir) || !fs.existsSync(distDir)) return true\n try {\n const distMtime = fs.statSync(distDir).mtimeMs\n const entries = fs.readdirSync(srcDir, { withFileTypes: true, recursive: true })\n for (const entry of entries) {\n if (!entry.isFile()) continue\n const filePath = path.join(entry.parentPath ?? (entry as any).path, entry.name)\n if (fs.statSync(filePath).mtimeMs > distMtime) return true\n }\n return false\n } catch {\n return true\n }\n}\n\n/**\n * Ensure a library dependency (not an addon) has a dist/ directory.\n * Does NOT rebuild — in dev mode, `npm run build` should be run separately.\n * Only checks that dist/ exists and has an index file.\n */\nexport function ensureLibraryBuilt(packageName: string, packagesDir: string): void {\n const dirName = packageName.replace('@camstack/', '')\n const sourceDir = path.join(packagesDir, dirName)\n if (!fs.existsSync(sourceDir)) return\n\n const distDir = path.join(sourceDir, 'dist')\n const hasIndex = fs.existsSync(path.join(distDir, 'index.js')) || fs.existsSync(path.join(distDir, 'index.mjs'))\n if (hasIndex) return\n\n console.warn(`[ensureLibraryBuilt] ${packageName} has no dist/ — run 'npm run build' first`)\n}\n\n/**\n * Install a single npm package into a target directory (package.json + dist/).\n * No validation on camstack.addons -- works for any @camstack/* package.\n * Uses synchronous child_process calls (suitable for first-boot and update paths).\n */\nexport function installPackageFromNpmSync(packageName: string, targetDir: string): void {\n const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'camstack-install-'))\n\n try {\n // npm pack downloads the tgz without installing\n const stdout = execFileSync('npm', ['pack', packageName, '--pack-destination', tmpDir], {\n timeout: 120_000,\n encoding: 'utf-8',\n })\n\n const tgzFilename = stdout.trim().split('\\n').pop()?.trim()\n if (!tgzFilename) throw new Error('npm pack produced no output')\n\n const tgzPath = path.join(tmpDir, tgzFilename)\n\n // Extract tgz\n const extractDir = path.join(tmpDir, 'extracted')\n ensureDir(extractDir)\n execFileSync('tar', ['-xzf', tgzPath, '-C', extractDir], { timeout: 30_000 })\n\n // npm pack creates a package/ subdirectory\n const packageSubDir = path.join(extractDir, 'package')\n const srcPkgJsonDir = fs.existsSync(path.join(packageSubDir, 'package.json'))\n ? packageSubDir\n : extractDir\n\n // Copy package.json + dist/ to target\n fs.rmSync(targetDir, { recursive: true, force: true })\n ensureDir(targetDir)\n fs.copyFileSync(path.join(srcPkgJsonDir, 'package.json'), path.join(targetDir, 'package.json'))\n\n const distSrc = path.join(srcPkgJsonDir, 'dist')\n if (fs.existsSync(distSrc)) {\n copyDirRecursive(distSrc, path.join(targetDir, 'dist'))\n }\n\n // Copy extra directories from \"files\" in package.json (e.g., python/)\n try {\n const npmPkg = JSON.parse(fs.readFileSync(path.join(srcPkgJsonDir, 'package.json'), 'utf-8'))\n copyExtraFileDirs(npmPkg, srcPkgJsonDir, targetDir)\n } catch { /* non-critical */ }\n } finally {\n fs.rmSync(tmpDir, { recursive: true, force: true })\n }\n}\n"],"mappings":"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","names":["fs","path","os"]}
@@ -0,0 +1,135 @@
1
+ // src/worker/worker-process-manager.ts
2
+ import { spawn } from "child_process";
3
+ var WorkerProcessManager = class {
4
+ constructor(sendToMain) {
5
+ this.sendToMain = sendToMain;
6
+ }
7
+ processes = /* @__PURE__ */ new Map();
8
+ async spawn(config) {
9
+ const child = spawn(config.command, [...config.args ?? []], {
10
+ cwd: config.cwd,
11
+ env: config.env ? { ...process.env, ...config.env } : void 0,
12
+ stdio: ["pipe", "pipe", "pipe"]
13
+ });
14
+ const managed = new ManagedProcess(child, config, this.sendToMain);
15
+ this.processes.set(child.pid, managed);
16
+ this.sendToMain({
17
+ type: "SUB_PROCESS_SPAWNED",
18
+ pid: child.pid,
19
+ name: config.name,
20
+ command: config.command
21
+ });
22
+ child.on("exit", (code) => {
23
+ this.sendToMain({
24
+ type: "SUB_PROCESS_EXITED",
25
+ pid: child.pid,
26
+ code
27
+ });
28
+ if (config.autoRestart && managed.restartCount < (config.maxRestarts ?? 3)) {
29
+ managed.restartCount++;
30
+ setTimeout(() => {
31
+ this.spawn(config).catch(() => {
32
+ });
33
+ }, 2e3);
34
+ }
35
+ });
36
+ return managed;
37
+ }
38
+ listProcesses() {
39
+ return [...this.processes.values()].map((p) => p.toInfo());
40
+ }
41
+ getWorkerStats() {
42
+ const mem = process.memoryUsage();
43
+ const cpu = process.cpuUsage();
44
+ return {
45
+ pid: process.pid,
46
+ cpuPercent: (cpu.user + cpu.system) / 1e6,
47
+ memoryRss: mem.rss,
48
+ heapUsed: mem.heapUsed,
49
+ uptimeSeconds: Math.round(process.uptime()),
50
+ restartCount: 0,
51
+ state: "running"
52
+ };
53
+ }
54
+ async killAll() {
55
+ for (const [, proc] of this.processes) {
56
+ proc.kill("SIGTERM");
57
+ }
58
+ this.processes.clear();
59
+ }
60
+ };
61
+ var ManagedProcess = class {
62
+ constructor(child, config, sendToMain) {
63
+ this.child = child;
64
+ this.config = config;
65
+ this.sendToMain = sendToMain;
66
+ this.pid = child.pid;
67
+ this.name = config.name;
68
+ child.on("exit", (code) => {
69
+ for (const handler of this.exitHandlers) handler(code);
70
+ });
71
+ child.on("error", (err) => {
72
+ for (const handler of this.errorHandlers) handler(err);
73
+ });
74
+ }
75
+ pid;
76
+ name;
77
+ restartCount = 0;
78
+ startedAt = Date.now();
79
+ exitHandlers = [];
80
+ errorHandlers = [];
81
+ getStats() {
82
+ return {
83
+ pid: this.pid,
84
+ cpuPercent: 0,
85
+ memoryRss: 0,
86
+ uptimeSeconds: Math.round((Date.now() - this.startedAt) / 1e3),
87
+ restartCount: this.restartCount,
88
+ state: this.child.exitCode !== null ? "stopped" : "running"
89
+ };
90
+ }
91
+ write(data) {
92
+ this.child.stdin?.write(data);
93
+ }
94
+ get stdout() {
95
+ return this.child.stdout;
96
+ }
97
+ get stderr() {
98
+ return this.child.stderr;
99
+ }
100
+ kill(signal = "SIGTERM") {
101
+ this.child.kill(signal);
102
+ }
103
+ wait() {
104
+ if (this.child.exitCode !== null) {
105
+ return Promise.resolve({ code: this.child.exitCode, signal: null });
106
+ }
107
+ return new Promise((resolve) => {
108
+ this.child.on("exit", (code, signal) => {
109
+ resolve({ code, signal });
110
+ });
111
+ });
112
+ }
113
+ onExit(handler) {
114
+ this.exitHandlers.push(handler);
115
+ }
116
+ onError(handler) {
117
+ this.errorHandlers.push(handler);
118
+ }
119
+ toInfo() {
120
+ return {
121
+ pid: this.pid,
122
+ name: this.name,
123
+ command: this.config.command,
124
+ state: this.child.exitCode !== null ? "stopped" : "running",
125
+ cpuPercent: 0,
126
+ memoryRss: 0,
127
+ uptimeSeconds: Math.round((Date.now() - this.startedAt) / 1e3)
128
+ };
129
+ }
130
+ };
131
+
132
+ export {
133
+ WorkerProcessManager
134
+ };
135
+ //# sourceMappingURL=chunk-S5YWNNPK.mjs.map