@qvac/sdk 0.12.2 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/NOTICE +55 -1225
  2. package/README.md +12 -0
  3. package/dist/client/api/bci-transcribe.d.ts +57 -0
  4. package/dist/client/api/bci-transcribe.js +172 -0
  5. package/dist/client/api/get-model-info.d.ts +1 -1
  6. package/dist/client/api/index.d.ts +1 -0
  7. package/dist/client/api/index.js +1 -0
  8. package/dist/client/api/video.d.ts +26 -6
  9. package/dist/client/api/video.js +30 -7
  10. package/dist/client/api/vla.d.ts +11 -7
  11. package/dist/client/api/vla.js +11 -7
  12. package/dist/client/rpc/bare-client.js +2 -2
  13. package/dist/commands/bundle/constants.js +2 -0
  14. package/dist/examples/bci/bci-filesystem-streaming.d.ts +2 -0
  15. package/dist/examples/bci/bci-filesystem-streaming.js +77 -0
  16. package/dist/examples/bci/bci-filesystem.d.ts +2 -0
  17. package/dist/examples/bci/bci-filesystem.js +63 -0
  18. package/dist/examples/cache-management.js +0 -1
  19. package/dist/examples/cancel-by-request-id.js +0 -1
  20. package/dist/examples/classification/classify-image.js +1 -1
  21. package/dist/examples/completion-events.js +6 -2
  22. package/dist/examples/completion-stop-reason.d.ts +25 -0
  23. package/dist/examples/completion-stop-reason.js +65 -0
  24. package/dist/examples/concurrent-completion-collision.d.ts +2 -0
  25. package/dist/examples/concurrent-completion-collision.js +98 -0
  26. package/dist/examples/config-reload.js +1 -2
  27. package/dist/examples/default-config-usage.js +0 -1
  28. package/dist/examples/delegated-inference/composite.js +0 -1
  29. package/dist/examples/delegated-inference/consumer-profiled.js +0 -1
  30. package/dist/examples/delegated-inference/consumer.js +0 -1
  31. package/dist/examples/diffusion-esrgan-upscale.js +1 -3
  32. package/dist/examples/diffusion-flux2-klein-img2img.js +1 -1
  33. package/dist/examples/diffusion-flux2-klein.js +2 -2
  34. package/dist/examples/diffusion-img2img.js +1 -1
  35. package/dist/examples/diffusion-img2vid.d.ts +2 -0
  36. package/dist/examples/diffusion-img2vid.js +78 -0
  37. package/dist/examples/diffusion-simple.js +1 -1
  38. package/dist/examples/diffusion-txt2img.js +8 -3
  39. package/dist/examples/diffusion-txt2vid.js +2 -3
  40. package/dist/examples/download-with-blind-relays.js +0 -1
  41. package/dist/examples/embed-multi-gpu.js +1 -2
  42. package/dist/examples/embed-p2p.js +4 -2
  43. package/dist/examples/finetune/llamacpp-finetune.js +5 -3
  44. package/dist/examples/kv-cache-cleanup.js +0 -1
  45. package/dist/examples/kv-cache-custom-key.js +0 -1
  46. package/dist/examples/kv-cache-example.js +4 -2
  47. package/dist/examples/llamacpp-cache.js +0 -1
  48. package/dist/examples/llamacpp-dynamic-tools.js +4 -2
  49. package/dist/examples/llamacpp-filesystem.js +1 -1
  50. package/dist/examples/llamacpp-http.js +1 -1
  51. package/dist/examples/llamacpp-multi-gpu.js +2 -2
  52. package/dist/examples/llamacpp-multimodal.js +0 -1
  53. package/dist/examples/llamacpp-p2p.js +0 -1
  54. package/dist/examples/llamacpp-sharded.js +1 -1
  55. package/dist/examples/llamacpp-structured-output.js +0 -1
  56. package/dist/examples/logging-file-transport.js +0 -1
  57. package/dist/examples/logging-streaming.js +0 -2
  58. package/dist/examples/mcp-websearch.js +0 -1
  59. package/dist/examples/multi-model-demo.js +0 -2
  60. package/dist/examples/ocr-fasttext.js +0 -1
  61. package/dist/examples/plugins.js +1 -4
  62. package/dist/examples/profiling/basic.js +3 -2
  63. package/dist/examples/profiling/per-call.js +0 -1
  64. package/dist/examples/quickstart.js +0 -1
  65. package/dist/examples/rag/rag-chromadb.js +0 -1
  66. package/dist/examples/rag/rag-hyperdb/cancellation.js +0 -1
  67. package/dist/examples/rag/rag-hyperdb/ingest.js +0 -1
  68. package/dist/examples/rag/rag-hyperdb/pipeline.js +0 -1
  69. package/dist/examples/rag/rag-hyperdb/workspaces.js +0 -1
  70. package/dist/examples/rag/rag-lancedb.js +0 -1
  71. package/dist/examples/rag/rag-sqlite.js +0 -1
  72. package/dist/examples/suspend-resume.js +0 -1
  73. package/dist/examples/tools/llamacpp-native-tools.js +0 -1
  74. package/dist/examples/tools/llamacpp-tools-gemma4.js +1 -1
  75. package/dist/examples/tools/llamacpp-tools-pythonic.js +2 -2
  76. package/dist/examples/tools/llamacpp-tools-qwen35.js +1 -1
  77. package/dist/examples/transcription/parakeet-ctc-filesystem.js +1 -1
  78. package/dist/examples/transcription/parakeet-microphone-record.js +0 -1
  79. package/dist/examples/transcription/parakeet-microphone-stream.js +0 -1
  80. package/dist/examples/transcription/parakeet-sortformer.js +1 -2
  81. package/dist/examples/transcription/parakeet-tdt-filesystem.js +1 -1
  82. package/dist/examples/transcription/whispercpp-filesystem-streaming.js +10 -6
  83. package/dist/examples/transcription/whispercpp-filesystem.js +4 -2
  84. package/dist/examples/transcription/whispercpp-microphone-conversation.js +0 -1
  85. package/dist/examples/transcription/whispercpp-microphone-record.js +0 -1
  86. package/dist/examples/transcription/whispercpp-prompt.js +0 -1
  87. package/dist/examples/translation/translation-bergamot-batch.js +1 -2
  88. package/dist/examples/translation/translation-bergamot-pivot.js +3 -4
  89. package/dist/examples/translation/translation-bergamot.js +1 -2
  90. package/dist/examples/translation/translation-indic.js +1 -2
  91. package/dist/examples/translation/translation-llm-afriquegemma.js +3 -4
  92. package/dist/examples/translation/translation-llm-context.js +1 -2
  93. package/dist/examples/translation/translation-llm.js +2 -3
  94. package/dist/examples/translation/translation-stream.js +2 -3
  95. package/dist/examples/tts/chatterbox.js +1 -2
  96. package/dist/examples/tts/llm-to-tts-streaming.js +2 -4
  97. package/dist/examples/tts/supertonic-multilingual.js +1 -2
  98. package/dist/examples/tts/supertonic.js +1 -2
  99. package/dist/examples/vla-pi05.d.ts +2 -0
  100. package/dist/examples/vla-pi05.js +84 -0
  101. package/dist/examples/vla-smolvla.js +1 -1
  102. package/dist/examples/voice-assistant/voice-assistant.js +1 -4
  103. package/dist/index.d.ts +2 -2
  104. package/dist/index.js +9 -7
  105. package/dist/models/registry/models.d.ts +869 -186
  106. package/dist/models/registry/models.js +3160 -2453
  107. package/dist/models/update-models/companions.d.ts +7 -0
  108. package/dist/models/update-models/companions.js +56 -0
  109. package/dist/models/update-models/naming.js +10 -0
  110. package/dist/pear/pre.js +2 -0
  111. package/dist/schemas/bci-config.d.ts +36 -0
  112. package/dist/schemas/bci-config.js +56 -0
  113. package/dist/schemas/bci.d.ts +177 -0
  114. package/dist/schemas/bci.js +95 -0
  115. package/dist/schemas/common.d.ts +395 -51
  116. package/dist/schemas/common.js +5 -0
  117. package/dist/schemas/completion-event.d.ts +1 -0
  118. package/dist/schemas/download-asset.d.ts +7 -3
  119. package/dist/schemas/embed.d.ts +2 -0
  120. package/dist/schemas/embed.js +1 -0
  121. package/dist/schemas/engine-addon-map.d.ts +1 -0
  122. package/dist/schemas/engine-addon-map.js +4 -1
  123. package/dist/schemas/get-model-info.d.ts +2 -0
  124. package/dist/schemas/get-model-info.js +1 -0
  125. package/dist/schemas/index.d.ts +2 -0
  126. package/dist/schemas/index.js +2 -0
  127. package/dist/schemas/llamacpp-config.d.ts +6 -2
  128. package/dist/schemas/load-model.d.ts +1841 -267
  129. package/dist/schemas/load-model.js +34 -1
  130. package/dist/schemas/model-src-utils.d.ts +10 -2
  131. package/dist/schemas/model-types.d.ts +19 -1
  132. package/dist/schemas/model-types.js +11 -0
  133. package/dist/schemas/ocr.d.ts +2 -0
  134. package/dist/schemas/plugin.d.ts +13 -5
  135. package/dist/schemas/plugin.js +13 -4
  136. package/dist/schemas/registry.d.ts +18 -0
  137. package/dist/schemas/registry.js +2 -0
  138. package/dist/schemas/reload-config.d.ts +10 -2
  139. package/dist/schemas/sdcpp-config.d.ts +75 -8
  140. package/dist/schemas/sdcpp-config.js +56 -10
  141. package/dist/schemas/sdk-config.d.ts +342 -0
  142. package/dist/schemas/text-to-speech.d.ts +8 -0
  143. package/dist/schemas/transcription-config.d.ts +2 -0
  144. package/dist/schemas/transcription.d.ts +12 -0
  145. package/dist/schemas/transcription.js +8 -0
  146. package/dist/schemas/translation-config.d.ts +44 -16
  147. package/dist/schemas/translation-config.js +2 -0
  148. package/dist/schemas/vla.d.ts +14 -0
  149. package/dist/schemas/vla.js +26 -3
  150. package/dist/server/bare/delegate-connect-diagnostics.d.ts +9 -0
  151. package/dist/server/bare/delegate-connect-diagnostics.js +44 -0
  152. package/dist/server/bare/delegate-rpc-client.js +10 -12
  153. package/dist/server/bare/hyperswarm.js +1 -0
  154. package/dist/server/bare/ops/bci-transcribe.d.ts +13 -0
  155. package/dist/server/bare/ops/bci-transcribe.js +248 -0
  156. package/dist/server/bare/ops/embed.js +2 -1
  157. package/dist/server/bare/ops/kv-cache-utils.js +4 -3
  158. package/dist/server/bare/ops/transcribe.js +6 -2
  159. package/dist/server/bare/ops/translate.js +1 -1
  160. package/dist/server/bare/plugins/bci-whispercpp-transcription/plugin.d.ts +130 -0
  161. package/dist/server/bare/plugins/bci-whispercpp-transcription/plugin.js +116 -0
  162. package/dist/server/bare/plugins/ggml-vla/ops/vla-run.js +4 -0
  163. package/dist/server/bare/plugins/ggml-vla/plugin.d.ts +7 -0
  164. package/dist/server/bare/plugins/ggml-vla/plugin.js +1 -1
  165. package/dist/server/bare/plugins/index.d.ts +1 -0
  166. package/dist/server/bare/plugins/index.js +1 -0
  167. package/dist/server/bare/plugins/llamacpp-completion/ops/finetune.js +22 -29
  168. package/dist/server/bare/plugins/llamacpp-completion/plugin.d.ts +2 -0
  169. package/dist/server/bare/plugins/llamacpp-completion/plugin.js +38 -7
  170. package/dist/server/bare/plugins/llamacpp-embedding/plugin.d.ts +1 -0
  171. package/dist/server/bare/plugins/llamacpp-embedding/plugin.js +9 -1
  172. package/dist/server/bare/plugins/nmtcpp-translation/plugin.d.ts +14 -0
  173. package/dist/server/bare/plugins/onnx-ocr/plugin.d.ts +2 -0
  174. package/dist/server/bare/plugins/parakeet-transcription/plugin.d.ts +8 -0
  175. package/dist/server/bare/plugins/sdcpp-generation/ops/video.js +7 -1
  176. package/dist/server/bare/plugins/sdcpp-generation/plugin.d.ts +58 -4
  177. package/dist/server/bare/plugins/sdcpp-generation/plugin.js +9 -4
  178. package/dist/server/bare/plugins/whispercpp-transcription/plugin.d.ts +10 -0
  179. package/dist/server/bare/registry/model-config-utils.js +3 -0
  180. package/dist/server/bare/runtime/request-registry-singleton.js +11 -4
  181. package/dist/server/bare/runtime/request-registry.d.ts +60 -16
  182. package/dist/server/bare/runtime/request-registry.js +258 -15
  183. package/dist/server/bare/runtime/with-request-context.d.ts +1 -1
  184. package/dist/server/bare/runtime/with-request-context.js +1 -1
  185. package/dist/server/bare/types/addon-responses.d.ts +5 -0
  186. package/dist/server/rpc/handler-registry.js +4 -0
  187. package/dist/server/rpc/handlers/bci-transcribe-stream.d.ts +3 -0
  188. package/dist/server/rpc/handlers/bci-transcribe-stream.js +4 -0
  189. package/dist/server/rpc/handlers/bci-transcribe.d.ts +3 -0
  190. package/dist/server/rpc/handlers/bci-transcribe.js +4 -0
  191. package/dist/server/rpc/handlers/download-asset.js +1 -1
  192. package/dist/server/rpc/handlers/get-model-info.js +3 -3
  193. package/dist/server/rpc/handlers/index.d.ts +4 -0
  194. package/dist/server/rpc/handlers/index.js +4 -0
  195. package/dist/server/rpc/handlers/load-model/download-manager.d.ts +1 -1
  196. package/dist/server/rpc/handlers/load-model/handler.js +1 -1
  197. package/dist/server/rpc/handlers/load-model/http.js +6 -6
  198. package/dist/server/rpc/handlers/load-model/hyperdrive.js +1 -0
  199. package/dist/server/rpc/handlers/load-model/registry-companion-set.d.ts +1 -0
  200. package/dist/server/rpc/handlers/load-model/registry-companion-set.js +2 -2
  201. package/dist/server/rpc/handlers/load-model/registry.js +1 -0
  202. package/dist/server/rpc/handlers/load-model/resolve-session.d.ts +1 -1
  203. package/dist/server/rpc/handlers/rag.js +5 -5
  204. package/dist/server/utils/checksum.js +2 -1
  205. package/dist/server/utils/formatting.js +2 -1
  206. package/dist/server/utils/multi-gpu-mobile.d.ts +5 -0
  207. package/dist/server/utils/multi-gpu-mobile.js +10 -0
  208. package/dist/server/worker.js +2 -1
  209. package/dist/utils/aggregate-events.d.ts +2 -1
  210. package/dist/utils/aggregate-events.js +11 -3
  211. package/dist/utils/errors-server.d.ts +6 -4
  212. package/dist/utils/errors-server.js +6 -4
  213. package/electron-forge/index.cjs +820 -0
  214. package/package.json +68 -35
  215. package/dist/scripts/run-security-tests-bare.d.ts +0 -2
  216. package/dist/scripts/run-security-tests-bare.js +0 -78
@@ -0,0 +1,820 @@
1
+ /**
2
+ * @qvac/sdk/electron-forge
3
+ *
4
+ * Electron Forge plugin: bundles the QVAC worker, verifies its native addons,
5
+ * then configures Electron Packager to tree-shake unused @qvac/* addons and
6
+ * non-target prebuilds.
7
+ *
8
+ * macOS universal (`arch: "universal"`) is not supported — native addon
9
+ * prebuilds are arch-specific. Build darwin-arm64 and darwin-x64 separately.
10
+ */
11
+
12
+ "use strict";
13
+
14
+ const { PluginBase } = require("@electron-forge/plugin-base");
15
+ const { createRequire } = require("module");
16
+ const path = require("path");
17
+ const fs = require("fs");
18
+
19
+ // ============================================
20
+ // Errors
21
+ // ============================================
22
+
23
+ /**
24
+ * Plugin-originated error. Stack is trimmed to `name: message` so Forge's
25
+ * "unhandled rejection" block stays focused on the cause.
26
+ */
27
+ class QvacForgePluginError extends Error {
28
+ constructor(message) {
29
+ super(message);
30
+ this.name = "QvacForgePluginError";
31
+ this.stack = `${this.name}: ${this.message}`;
32
+ }
33
+ }
34
+
35
+ // ============================================
36
+ // Logger
37
+ // ============================================
38
+
39
+ const PREFIX = "[qvac:electron-forge]";
40
+
41
+ const EXPECTED_FS_ERROR_CODES = new Set([
42
+ "ENOENT",
43
+ "EACCES",
44
+ "EPERM",
45
+ "ENOTDIR",
46
+ ]);
47
+
48
+ const LOG_LEVELS = {
49
+ off: 0,
50
+ error: 1,
51
+ warn: 2,
52
+ info: 3,
53
+ debug: 4,
54
+ };
55
+
56
+ function getDefaultLevel() {
57
+ const level = process.env.QVAC_LOG_LEVEL?.toLowerCase();
58
+ return level && level in LOG_LEVELS ? level : "info";
59
+ }
60
+
61
+ let currentLevel = LOG_LEVELS[getDefaultLevel()];
62
+
63
+ function setLogLevel(level) {
64
+ if (!(level in LOG_LEVELS)) {
65
+ console.warn(
66
+ `${PREFIX} Invalid log level "${level}", using "info". Valid: ${Object.keys(LOG_LEVELS).join(", ")}`,
67
+ );
68
+ currentLevel = LOG_LEVELS.info;
69
+ return;
70
+ }
71
+ currentLevel = LOG_LEVELS[level];
72
+ }
73
+
74
+ const logger = {
75
+ error(msg) {
76
+ if (currentLevel >= LOG_LEVELS.error) console.error(PREFIX, msg);
77
+ },
78
+ warn(msg) {
79
+ if (currentLevel >= LOG_LEVELS.warn) console.warn(PREFIX, msg);
80
+ },
81
+ info(msg) {
82
+ if (currentLevel >= LOG_LEVELS.info) console.log(PREFIX, msg);
83
+ },
84
+ debug(msg) {
85
+ if (currentLevel >= LOG_LEVELS.debug) console.log(PREFIX, msg);
86
+ },
87
+ fsError(context, err) {
88
+ if (err && EXPECTED_FS_ERROR_CODES.has(err.code)) return;
89
+ this.warn(`Unexpected error in ${context}: ${err?.message || err}`);
90
+ },
91
+ };
92
+
93
+ // ============================================
94
+ // SDK Package Resolution (for addon discovery)
95
+ // ============================================
96
+
97
+ const SDK_PACKAGE_NAMES = ["@qvac/sdk"];
98
+
99
+ function resolveSDKPackage(startDir) {
100
+ for (const name of SDK_PACKAGE_NAMES) {
101
+ try {
102
+ const pkgPath = require.resolve(`${name}/package`, { paths: [startDir] });
103
+ return { name, path: pkgPath };
104
+ } catch {
105
+ // Try next package name
106
+ }
107
+ }
108
+ return null;
109
+ }
110
+
111
+ function isDir(dirPath) {
112
+ try {
113
+ return fs.statSync(dirPath).isDirectory();
114
+ } catch (err) {
115
+ logger.fsError("isDir", err);
116
+ return false;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Finds the @qvac scope directory using Node's module resolution.
122
+ * Handles monorepos, workspaces, and hoisted layouts.
123
+ */
124
+ function findQvacScopeDir(startDir) {
125
+ const sdkPkg = resolveSDKPackage(startDir);
126
+ if (!sdkPkg) {
127
+ throw new QvacForgePluginError(
128
+ `Could not find QVAC SDK. ` +
129
+ `Ensure one of [${SDK_PACKAGE_NAMES.join(", ")}] is installed.`,
130
+ );
131
+ }
132
+
133
+ logger.debug(`Resolved SDK package: ${sdkPkg.name}`);
134
+
135
+ const baseDir = path.resolve(startDir);
136
+ const req = createRequire(path.join(baseDir, "package.json"));
137
+ const nodeModulesDirs = req.resolve.paths(sdkPkg.name) || [];
138
+
139
+ for (const nodeModulesDir of nodeModulesDirs) {
140
+ const scopeDir = path.join(nodeModulesDir, "@qvac");
141
+ if (isDir(scopeDir)) return scopeDir;
142
+ }
143
+
144
+ // Fallback: derive from SDK path for flat node_modules layouts.
145
+ const derived = path.dirname(path.dirname(sdkPkg.path));
146
+ if (path.basename(derived) === "@qvac" && isDir(derived)) return derived;
147
+
148
+ throw new QvacForgePluginError(
149
+ `Could not find @qvac packages. ` +
150
+ `Ensure dependencies are installed under node_modules (PnP is not supported).`,
151
+ );
152
+ }
153
+
154
+ /**
155
+ * Discovers installed @qvac addon packages by scanning node_modules/@qvac
156
+ * for packages that have `addon: true` in package.json.
157
+ */
158
+ function discoverQvacAddonPackages(projectDir) {
159
+ let scopeDir;
160
+ try {
161
+ scopeDir = findQvacScopeDir(projectDir);
162
+ } catch (err) {
163
+ logger.warn(err.message);
164
+ return [];
165
+ }
166
+
167
+ let entries;
168
+ try {
169
+ entries = fs.readdirSync(scopeDir);
170
+ } catch (err) {
171
+ logger.fsError("discoverQvacAddonPackages", err);
172
+ return [];
173
+ }
174
+
175
+ const discovered = [];
176
+
177
+ for (const name of entries) {
178
+ const pkgDir = path.join(scopeDir, name);
179
+ if (!isDir(pkgDir)) continue;
180
+
181
+ const pkgJsonPath = path.join(pkgDir, "package.json");
182
+ if (!fs.existsSync(pkgJsonPath)) continue;
183
+
184
+ try {
185
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
186
+ if (pkg.addon === true) {
187
+ discovered.push(`@qvac/${name}`);
188
+ }
189
+ } catch (err) {
190
+ logger.warn(`Failed to parse ${pkgJsonPath}: ${err.message}`);
191
+ }
192
+ }
193
+
194
+ discovered.sort();
195
+ return discovered;
196
+ }
197
+
198
+ /**
199
+ * Pure diff: addons that are installed but not in the required set.
200
+ * Exposed for unit testing.
201
+ */
202
+ function diffAddons(installed, required) {
203
+ const requiredSet = new Set(required);
204
+ const exclusions = [];
205
+ for (const pkg of installed) {
206
+ if (!requiredSet.has(pkg)) exclusions.push(pkg);
207
+ }
208
+ return exclusions;
209
+ }
210
+
211
+ /**
212
+ * Computes the list of installed @qvac addons that aren't in `required`.
213
+ * Logs include/exclude decisions for each discovered package.
214
+ */
215
+ function computeExclusions(required, projectDir) {
216
+ const installed = discoverQvacAddonPackages(projectDir);
217
+
218
+ if (installed.length === 0) {
219
+ logger.warn(
220
+ "No @qvac addon packages discovered. Skipping addon exclusions.",
221
+ );
222
+ return [];
223
+ }
224
+
225
+ const requiredSet = new Set(required);
226
+ const exclusions = [];
227
+ for (const pkg of installed) {
228
+ if (requiredSet.has(pkg)) {
229
+ logger.info(`Including required addon: ${pkg}`);
230
+ } else {
231
+ logger.info(`Excluding unused addon: ${pkg}`);
232
+ exclusions.push(pkg);
233
+ }
234
+ }
235
+ return exclusions;
236
+ }
237
+
238
+ // ============================================
239
+ // Ignore Patterns
240
+ // ============================================
241
+
242
+ /** Mobile prebuild patterns to always exclude in desktop builds. */
243
+ const MOBILE_PREBUILD_PATTERNS = [
244
+ /[\\/]prebuilds[\\/]android-/,
245
+ /[\\/]prebuilds[\\/]ios-/,
246
+ ];
247
+
248
+ /** Forge's default output dir — always exclude to avoid recursive packaging. */
249
+ const OUT_DIR_PATTERN = /^\/out\//;
250
+
251
+ function toArray(value) {
252
+ if (value === undefined) return [];
253
+ return Array.isArray(value) ? value : [value];
254
+ }
255
+
256
+ function escapeRegExp(str) {
257
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
258
+ }
259
+
260
+ function createAddonIgnorePatterns(exclusions) {
261
+ const patterns = [];
262
+ for (const addon of exclusions) {
263
+ const parts = addon.split("/").map(escapeRegExp);
264
+ patterns.push(
265
+ new RegExp(
266
+ `[\\\\/]node_modules[\\\\/]${parts.join("[\\\\/]")}([\\\\/]|$)`,
267
+ ),
268
+ );
269
+ }
270
+ return patterns;
271
+ }
272
+
273
+ /**
274
+ * Builds an Electron Packager `ignore` value that excludes the given addon
275
+ * packages and mobile prebuilds, while composing with any user-provided
276
+ * ignore (function or regex array).
277
+ */
278
+ function createIgnore(exclusions, existingIgnore) {
279
+ const addonIgnorePatterns = createAddonIgnorePatterns(exclusions);
280
+
281
+ if (typeof existingIgnore === "function") {
282
+ return (filePath) => {
283
+ if (existingIgnore(filePath)) return true;
284
+ if (OUT_DIR_PATTERN.test(filePath)) return true;
285
+ for (const pattern of addonIgnorePatterns) {
286
+ if (pattern.test(filePath)) return true;
287
+ }
288
+ for (const pattern of MOBILE_PREBUILD_PATTERNS) {
289
+ if (pattern.test(filePath)) return true;
290
+ }
291
+ return false;
292
+ };
293
+ }
294
+
295
+ return [
296
+ ...toArray(existingIgnore),
297
+ OUT_DIR_PATTERN,
298
+ ...MOBILE_PREBUILD_PATTERNS,
299
+ ...addonIgnorePatterns,
300
+ ];
301
+ }
302
+
303
+ // ============================================
304
+ // Prebuild Pruning
305
+ // ============================================
306
+
307
+ /**
308
+ * Recursively finds all prebuilds directories under a root path, including
309
+ * inside nested node_modules. Nested node_modules show up in real-world npm
310
+ * trees whenever a transitive dep can't be hoisted (e.g. bare-tls under
311
+ * bare-fetch when version constraints conflict). Skipping those misses real
312
+ * cross-platform prebuilds that would otherwise ship in the packaged app.
313
+ */
314
+ function findPrebuildsDirs(rootPath) {
315
+ const results = [];
316
+
317
+ function walk(dir) {
318
+ let entries;
319
+ try {
320
+ entries = fs.readdirSync(dir, { withFileTypes: true });
321
+ } catch (err) {
322
+ logger.fsError("findPrebuildsDirs", err);
323
+ return;
324
+ }
325
+
326
+ for (const entry of entries) {
327
+ if (!entry.isDirectory()) continue;
328
+ const fullPath = path.join(dir, entry.name);
329
+
330
+ if (entry.name === "prebuilds") {
331
+ results.push(fullPath);
332
+ // Don't recurse into prebuilds — its children are arch-keyed leaves.
333
+ continue;
334
+ }
335
+
336
+ walk(fullPath);
337
+ }
338
+ }
339
+
340
+ walk(rootPath);
341
+ return results;
342
+ }
343
+
344
+ function getDirSize(dirPath) {
345
+ let size = 0;
346
+
347
+ function walk(dir) {
348
+ let entries;
349
+ try {
350
+ entries = fs.readdirSync(dir, { withFileTypes: true });
351
+ } catch (err) {
352
+ logger.fsError("getDirSize", err);
353
+ return;
354
+ }
355
+
356
+ for (const entry of entries) {
357
+ const fullPath = path.join(dir, entry.name);
358
+ if (entry.isDirectory()) {
359
+ walk(fullPath);
360
+ } else {
361
+ try {
362
+ size += fs.statSync(fullPath).size;
363
+ } catch (err) {
364
+ logger.fsError("getDirSize.stat", err);
365
+ }
366
+ }
367
+ }
368
+ }
369
+
370
+ walk(dirPath);
371
+ return size;
372
+ }
373
+
374
+ /** Prunes prebuilds for a given path, keeping only target platform-arch. */
375
+ function prunePrebuildsForPath(buildPath, platform, arch) {
376
+ const nodeModulesPath = path.join(buildPath, "node_modules");
377
+
378
+ if (!fs.existsSync(nodeModulesPath)) {
379
+ logger.debug("No node_modules found, skipping prebuild pruning.");
380
+ return { deleted: 0, bytes: 0 };
381
+ }
382
+
383
+ const keepPrefix = `${platform}-${arch}`;
384
+ logger.debug(`Keeping prefix: ${keepPrefix}`);
385
+
386
+ const prebuildsDirs = findPrebuildsDirs(nodeModulesPath);
387
+ let totalDeleted = 0;
388
+ let totalBytes = 0;
389
+
390
+ for (const prebuildsDir of prebuildsDirs) {
391
+ let entries;
392
+ try {
393
+ entries = fs.readdirSync(prebuildsDir, { withFileTypes: true });
394
+ } catch (err) {
395
+ logger.fsError("prunePrebuildsForPath", err);
396
+ continue;
397
+ }
398
+
399
+ for (const entry of entries) {
400
+ if (!entry.isDirectory()) continue;
401
+
402
+ // Prefix match catches musl variants etc. (e.g. linux-x64-musl).
403
+ const shouldKeep = entry.name.startsWith(keepPrefix);
404
+
405
+ if (!shouldKeep) {
406
+ const fullPath = path.join(prebuildsDir, entry.name);
407
+ try {
408
+ const size = getDirSize(fullPath);
409
+ totalBytes += size;
410
+ fs.rmSync(fullPath, { recursive: true, force: true });
411
+ totalDeleted++;
412
+ logger.debug(`Deleted: ${entry.name}`);
413
+ } catch (err) {
414
+ logger.warn(`Failed to delete ${fullPath}: ${err.message}`);
415
+ }
416
+ }
417
+ }
418
+ }
419
+
420
+ return { deleted: totalDeleted, bytes: totalBytes };
421
+ }
422
+
423
+ /**
424
+ * Removes excluded addon directories from the packaged node_modules.
425
+ *
426
+ * Electron Packager's `ignore` filter excludes file contents but leaves the
427
+ * empty parent directory in the output. This sweep removes those shells so
428
+ * the packaged tree matches the manifest exactly. Idempotent.
429
+ */
430
+ function removeExcludedAddonDirs(buildPath, exclusions) {
431
+ const nodeModulesPath = path.join(buildPath, "node_modules");
432
+
433
+ if (!fs.existsSync(nodeModulesPath)) {
434
+ return { removed: 0, bytes: 0 };
435
+ }
436
+
437
+ let removed = 0;
438
+ let bytes = 0;
439
+
440
+ for (const name of exclusions) {
441
+ const pkgDir = path.join(nodeModulesPath, name);
442
+ if (!fs.existsSync(pkgDir)) continue;
443
+
444
+ try {
445
+ const size = getDirSize(pkgDir);
446
+ bytes += size;
447
+ fs.rmSync(pkgDir, { recursive: true, force: true });
448
+ removed += 1;
449
+ logger.debug(`Removed excluded addon dir: ${name}`);
450
+ } catch (err) {
451
+ logger.warn(`Failed to remove ${pkgDir}: ${err.message}`);
452
+ }
453
+ }
454
+
455
+ return { removed, bytes };
456
+ }
457
+
458
+ // ============================================
459
+ // SDK Commands Bridge (CJS plugin → ESM SDK commands)
460
+ // ============================================
461
+
462
+ /**
463
+ * Lazy dynamic-imports `@qvac/sdk/commands`. Plugin file is CJS for Forge's
464
+ * `require()`-based loader; the SDK's commands module is ESM-only via the
465
+ * package's "import" condition. Dynamic `import()` bridges them.
466
+ *
467
+ * @returns {Promise<{
468
+ * bundleSdk: Function,
469
+ * verifyBundle: Function,
470
+ * hasErrors: Function,
471
+ * formatVerifyBundleResult: Function,
472
+ * }>}
473
+ */
474
+ async function loadSdkCommands() {
475
+ try {
476
+ return await import("@qvac/sdk/commands");
477
+ } catch (err) {
478
+ throw new QvacForgePluginError(
479
+ `Could not load @qvac/sdk/commands: ${err?.message || err}. ` +
480
+ `Ensure @qvac/sdk is installed and exposes the ./commands subpath ` +
481
+ `(requires SDK >= 0.12.0).`,
482
+ );
483
+ }
484
+ }
485
+
486
+ /** Defaults to verifying against the host running Forge. */
487
+ function defaultHosts() {
488
+ return [`${process.platform}-${process.arch}`];
489
+ }
490
+
491
+ /**
492
+ * Single source of truth for the hosts list used by both bundleSdk and
493
+ * verifyBundle. Keeping these in sync matters: bundleSdk's `hosts` drives
494
+ * the bare-pack content (which prebuilds end up in the bundle), and
495
+ * verifyBundle checks the same set. Passing different lists silently
496
+ * produces inconsistent builds — bundle for one set, verify for another.
497
+ *
498
+ * @param {string[]|null|undefined} explicitHosts
499
+ * @returns {string[]}
500
+ */
501
+ function resolveHosts(explicitHosts) {
502
+ return Array.isArray(explicitHosts) && explicitHosts.length > 0
503
+ ? explicitHosts
504
+ : defaultHosts();
505
+ }
506
+
507
+ /**
508
+ * Detects target packaging hosts from Forge config + CLI argv. Returns null
509
+ * when nothing's specified so callers can fall back to defaultHosts().
510
+ *
511
+ * Forge populates packagerConfig from the config file but threads CLI
512
+ * `--platform`/`--arch` separately into Packager — at resolveForgeConfig time
513
+ * argv is the only signal we have. Mirrors checkForUniversalArch's parsing.
514
+ *
515
+ * Comma-separated `--arch=arm64,x64` expands to one host per arch.
516
+ *
517
+ * @param {object} forgeConfig
518
+ * @param {string[]} [argv] Override for testing (default: process.argv.slice(2)).
519
+ * @returns {string[]|null}
520
+ */
521
+ function detectTargetHosts(forgeConfig, argv = process.argv.slice(2)) {
522
+ let platform = forgeConfig?.packagerConfig?.platform || null;
523
+ let archSpec = forgeConfig?.packagerConfig?.arch || null;
524
+
525
+ for (let i = 0; i < argv.length; i += 1) {
526
+ const arg = argv[i];
527
+
528
+ if (arg === "--platform" || arg === "-p") {
529
+ platform = argv[i + 1] || platform;
530
+ continue;
531
+ }
532
+ if (typeof arg === "string" && arg.startsWith("--platform=")) {
533
+ platform = arg.slice("--platform=".length) || platform;
534
+ continue;
535
+ }
536
+ if (arg === "--arch" || arg === "-a") {
537
+ archSpec = argv[i + 1] || archSpec;
538
+ continue;
539
+ }
540
+ if (typeof arg === "string" && arg.startsWith("--arch=")) {
541
+ archSpec = arg.slice("--arch=".length) || archSpec;
542
+ }
543
+ }
544
+
545
+ if (!platform && !archSpec) return null;
546
+
547
+ const p = platform || process.platform;
548
+ const archs = (archSpec || process.arch)
549
+ .split(",")
550
+ .map((s) => s.trim())
551
+ .filter(Boolean);
552
+
553
+ return archs.length > 0 ? archs.map((a) => `${p}-${a}`) : null;
554
+ }
555
+
556
+ /**
557
+ * Runs `bundleSdk` then `verifyBundle` with the same resolved `hosts` list.
558
+ * Throws QvacForgePluginError on bundle failure or verify errors.
559
+ *
560
+ * Commands are injected (rather than dynamically imported here) so tests
561
+ * can assert the host-list wiring without touching the SDK runtime.
562
+ *
563
+ * @param {{ bundleSdk: Function, verifyBundle: Function, hasErrors: Function, formatVerifyBundleResult: Function }} commands
564
+ * @param {string} projectDir
565
+ * @param {{ configPath?: string|null, hosts?: string[]|null }} options
566
+ * @returns {Promise<{ addons: string[], bundlePath: string, manifestPath: string }>}
567
+ */
568
+ async function runBundleAndVerify(commands, projectDir, options) {
569
+ const { bundleSdk, verifyBundle, hasErrors, formatVerifyBundleResult } =
570
+ commands;
571
+
572
+ const hosts = resolveHosts(options.hosts);
573
+
574
+ logger.info(`Running bundleSdk (hosts: ${hosts.join(", ")})...`);
575
+ let bundleResult;
576
+ try {
577
+ const bundleOpts = { projectRoot: projectDir, hosts };
578
+ if (options.configPath) bundleOpts.configPath = options.configPath;
579
+ bundleResult = await bundleSdk(bundleOpts);
580
+ } catch (err) {
581
+ throw new QvacForgePluginError(
582
+ `bundleSdk failed: ${err?.message || err}`,
583
+ );
584
+ }
585
+
586
+ const addonCount = bundleResult.addons.length;
587
+ logger.info(
588
+ `bundleSdk: ${addonCount} native addon${addonCount === 1 ? "" : "s"} in bundle`,
589
+ );
590
+
591
+ logger.info(`Running verifyBundle (hosts: ${hosts.join(", ")})...`);
592
+ let verifyResult;
593
+ try {
594
+ const verifyOpts = {
595
+ projectRoot: projectDir,
596
+ addonsSource: bundleResult.bundlePath,
597
+ hosts,
598
+ };
599
+ if (options.configPath) verifyOpts.configPath = options.configPath;
600
+ verifyResult = await verifyBundle(verifyOpts);
601
+ } catch (err) {
602
+ throw new QvacForgePluginError(
603
+ `verifyBundle threw: ${err?.message || err}`,
604
+ );
605
+ }
606
+
607
+ if (hasErrors(verifyResult)) {
608
+ throw new QvacForgePluginError(
609
+ `verifyBundle reported errors:\n${formatVerifyBundleResult(verifyResult)}`,
610
+ );
611
+ }
612
+
613
+ if (verifyResult.issues.length > 0) {
614
+ logger.warn(
615
+ `verifyBundle produced warnings:\n${formatVerifyBundleResult(verifyResult)}`,
616
+ );
617
+ } else {
618
+ logger.info(
619
+ `verifyBundle: ${verifyResult.addons.length} addon${verifyResult.addons.length === 1 ? "" : "s"} OK`,
620
+ );
621
+ }
622
+
623
+ return bundleResult;
624
+ }
625
+
626
+ // ============================================
627
+ // QVAC Forge Plugin
628
+ // ============================================
629
+
630
+ class QvacForgePlugin extends PluginBase {
631
+ name = "qvac";
632
+
633
+ /**
634
+ * @param {object} [config]
635
+ * @param {string} [config.projectDir] Project root (default: process.cwd()).
636
+ * @param {string} [config.configPath] Path to a qvac config file (`.ts`, `.mjs`, `.js`, or `.json`). Forwarded to bundleSdk + verifyBundle. Default: auto-discover in `projectDir`.
637
+ * @param {string[]} [config.hosts] Hosts to verify against (default: `${process.platform}-${process.arch}`).
638
+ * @param {"off"|"error"|"warn"|"info"|"debug"} [config.logLevel]
639
+ */
640
+ constructor(config = {}) {
641
+ super(config);
642
+ this.projectDir = config.projectDir || process.cwd();
643
+ this.configPath = config.configPath || null;
644
+ this.hosts = Array.isArray(config.hosts) ? config.hosts : null;
645
+
646
+ if (config.logLevel) setLogLevel(config.logLevel);
647
+
648
+ // Cache for resolveForgeConfig double-fire (package + make).
649
+ this._cache = null;
650
+ this._pruneHook = null;
651
+
652
+ logger.debug("QvacForgePlugin initialized");
653
+ logger.debug(`Project directory: ${this.projectDir}`);
654
+ }
655
+
656
+ getHooks() {
657
+ return {
658
+ resolveForgeConfig: this.configurePackager.bind(this),
659
+ };
660
+ }
661
+
662
+ async configurePackager(forgeConfig) {
663
+ logger.info("Configuring packager for QVAC...");
664
+
665
+ if (!forgeConfig.packagerConfig) {
666
+ forgeConfig.packagerConfig = {};
667
+ }
668
+
669
+ // 1. Block macOS universal builds early.
670
+ this.checkForUniversalArch(forgeConfig);
671
+
672
+ // 2. Force asar: false (Bare worker can't load from asar). Truthy check
673
+ // catches both `asar: true` and `asar: { unpack: ... }` object configs.
674
+ if (forgeConfig.packagerConfig.asar) {
675
+ logger.warn(
676
+ "asar is enabled — Bare worker may fail to load. Overriding to false.",
677
+ );
678
+ }
679
+ forgeConfig.packagerConfig.asar = false;
680
+
681
+ // 3. Bundle + verify (cached across resolveForgeConfig invocations).
682
+ // `hosts` resolution: explicit config wins, then CLI/config-derived
683
+ // target, then host fallback inside runBundleAndVerify. Hosts are
684
+ // threaded into BOTH bundleSdk (drives bare-pack content) and
685
+ // verifyBundle (asserts prebuild availability) — passing different
686
+ // sets silently produces inconsistent builds.
687
+ if (this._cache === null) {
688
+ const detected = this.hosts ? null : detectTargetHosts(forgeConfig);
689
+ if (detected && !this.hosts) {
690
+ logger.info(`Detected target hosts from CLI/config: ${detected.join(", ")}`);
691
+ }
692
+ const commands = await loadSdkCommands();
693
+ const bundleResult = await runBundleAndVerify(
694
+ commands,
695
+ this.projectDir,
696
+ {
697
+ configPath: this.configPath,
698
+ hosts: this.hosts || detected,
699
+ },
700
+ );
701
+ const exclusions = computeExclusions(
702
+ bundleResult.addons,
703
+ this.projectDir,
704
+ );
705
+ this._cache = { bundleResult, exclusions };
706
+ } else {
707
+ logger.debug("Reusing cached bundleSdk result.");
708
+ }
709
+ const { exclusions } = this._cache;
710
+
711
+ // 4. Merge ignore patterns to exclude unused addons + mobile prebuilds.
712
+ const existingIgnore = forgeConfig.packagerConfig.ignore;
713
+ forgeConfig.packagerConfig.ignore = createIgnore(
714
+ exclusions,
715
+ existingIgnore,
716
+ );
717
+
718
+ // 5. afterPrune hook for prebuild pruning + excluded addon dir cleanup.
719
+ // Forge calls resolveForgeConfig multiple times during `make` (once for
720
+ // `Loading configuration`, then again for `Preparing to package`). We
721
+ // must not append the hook on each pass, otherwise Packager's afterPrune
722
+ // fires it N times and we walk the already-pruned tree on every repeat.
723
+ if (this._pruneHook === null) {
724
+ this._pruneHook = this.createPruneHook(exclusions);
725
+ }
726
+ const existingAfterPrune = toArray(forgeConfig.packagerConfig.afterPrune);
727
+ if (!existingAfterPrune.includes(this._pruneHook)) {
728
+ forgeConfig.packagerConfig.afterPrune = [
729
+ ...existingAfterPrune,
730
+ this._pruneHook,
731
+ ];
732
+ }
733
+
734
+ logger.debug("Packager configuration complete");
735
+ return forgeConfig;
736
+ }
737
+
738
+ /**
739
+ * Blocks macOS universal builds (darwin/universal).
740
+ *
741
+ * Native addon prebuilds are arch-specific (e.g. darwin-arm64 vs
742
+ * darwin-x64) and are not produced as a single "universal" prebuild. A
743
+ * universal Electron app would need to ship both arch prebuild sets,
744
+ * which is incompatible with this plugin's single-arch pruning model.
745
+ * Forge expands universal targets before Packager hooks run, so we must
746
+ * detect it before pruning runs.
747
+ */
748
+ checkForUniversalArch(forgeConfig) {
749
+ const universalMessage =
750
+ `macOS universal packaging is not supported by @qvac/sdk/electron-forge. ` +
751
+ `Native addon prebuilds are architecture-specific and this plugin currently only supports single-arch packaging. ` +
752
+ `Build separate darwin-arm64 and darwin-x64 packages instead.`;
753
+
754
+ if (forgeConfig?.packagerConfig?.arch === "universal") {
755
+ throw new QvacForgePluginError(universalMessage);
756
+ }
757
+
758
+ const args = process.argv.slice(2);
759
+ let arch = null;
760
+ for (let i = 0; i < args.length; i += 1) {
761
+ const arg = args[i];
762
+
763
+ if (arg === "--arch" || arg === "-a") {
764
+ arch = args[i + 1] || null;
765
+ continue;
766
+ }
767
+
768
+ if (typeof arg === "string" && arg.startsWith("--arch=")) {
769
+ arch = arg.slice("--arch=".length) || null;
770
+ continue;
771
+ }
772
+ }
773
+
774
+ if (arch === "universal") {
775
+ throw new QvacForgePluginError(universalMessage);
776
+ }
777
+ }
778
+
779
+ createPruneHook(exclusions = []) {
780
+ return (buildPath, electronVersion, platform, arch, done) => {
781
+ logger.info(`Pruning prebuilds for ${platform}-${arch}...`);
782
+
783
+ try {
784
+ const prebuildResult = prunePrebuildsForPath(buildPath, platform, arch);
785
+ const prebuildMb = (prebuildResult.bytes / 1024 / 1024).toFixed(1);
786
+ logger.info(
787
+ `Pruned ${prebuildResult.deleted} prebuild dirs (~${prebuildMb} MB reclaimed)`,
788
+ );
789
+
790
+ if (exclusions.length > 0) {
791
+ const cleanup = removeExcludedAddonDirs(buildPath, exclusions);
792
+ const cleanupMb = (cleanup.bytes / 1024 / 1024).toFixed(1);
793
+ logger.info(
794
+ `Removed ${cleanup.removed} excluded addon dir${cleanup.removed === 1 ? "" : "s"} (~${cleanupMb} MB reclaimed)`,
795
+ );
796
+ }
797
+
798
+ done();
799
+ } catch (err) {
800
+ logger.error(`Prebuild pruning failed: ${err.message}`);
801
+ done(err);
802
+ }
803
+ };
804
+ }
805
+ }
806
+
807
+ // ============================================
808
+ // Exports
809
+ // ============================================
810
+
811
+ module.exports = QvacForgePlugin;
812
+ module.exports.setLogLevel = setLogLevel;
813
+ module.exports.QvacForgePluginError = QvacForgePluginError;
814
+
815
+ // Internal helpers exposed for unit tests. Not part of the stable API.
816
+ module.exports.createIgnore = createIgnore;
817
+ module.exports.diffAddons = diffAddons;
818
+ module.exports.detectTargetHosts = detectTargetHosts;
819
+ module.exports.resolveHosts = resolveHosts;
820
+ module.exports.runBundleAndVerify = runBundleAndVerify;