sparkbun 0.1.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 (131) hide show
  1. package/bin/sparkbun.cjs +18 -0
  2. package/dist-linux-arm64/bsdiff +0 -0
  3. package/dist-linux-arm64/bspatch +0 -0
  4. package/dist-linux-arm64/libElectrobunCore.so +0 -0
  5. package/dist-linux-arm64/libNativeWrapper.so +0 -0
  6. package/dist-linux-arm64/libasar.so +0 -0
  7. package/dist-linux-x64/bsdiff +0 -0
  8. package/dist-linux-x64/bspatch +0 -0
  9. package/dist-linux-x64/libElectrobunCore.so +0 -0
  10. package/dist-linux-x64/libNativeWrapper.so +0 -0
  11. package/dist-linux-x64/libasar.so +0 -0
  12. package/dist-macos-arm64/bsdiff +0 -0
  13. package/dist-macos-arm64/bspatch +0 -0
  14. package/dist-macos-arm64/libElectrobunCore.dylib +0 -0
  15. package/dist-macos-arm64/libNativeWrapper.dylib +0 -0
  16. package/dist-macos-arm64/libasar.dylib +0 -0
  17. package/dist-macos-arm64/libwebgpu_dawn.dylib +0 -0
  18. package/dist-macos-arm64/preload-full.js +885 -0
  19. package/dist-macos-arm64/preload-sandboxed.js +111 -0
  20. package/dist-macos-arm64/process_helper +0 -0
  21. package/dist-win-x64/ElectrobunCore.dll +0 -0
  22. package/dist-win-x64/WebView2Loader.dll +0 -0
  23. package/dist-win-x64/bsdiff.exe +0 -0
  24. package/dist-win-x64/bspatch.exe +0 -0
  25. package/dist-win-x64/libNativeWrapper.dll +0 -0
  26. package/dist-win-x64/zig-asar/arm64/libasar.dll +0 -0
  27. package/dist-win-x64/zig-asar/x64/libasar.dll +0 -0
  28. package/package.json +47 -0
  29. package/scripts/build-and-upload-artifacts.js +207 -0
  30. package/scripts/gen-webgpu-ffi.mjs +162 -0
  31. package/scripts/install-windows-deps.ps1 +80 -0
  32. package/scripts/package-release.js +237 -0
  33. package/scripts/push-version.js +84 -0
  34. package/scripts/update-bun-version.ts +122 -0
  35. package/scripts/update-cef-version.ts +145 -0
  36. package/src/browser/builtinrpcSchema.ts +19 -0
  37. package/src/browser/global.d.ts +36 -0
  38. package/src/browser/index.ts +234 -0
  39. package/src/browser/webviewtag.ts +88 -0
  40. package/src/browser/wgputag.ts +48 -0
  41. package/src/bun/SparkBunConfig.ts +497 -0
  42. package/src/bun/__tests__/ffi-contract.test.ts +105 -0
  43. package/src/bun/core/ApplicationMenu.ts +70 -0
  44. package/src/bun/core/BrowserView.ts +416 -0
  45. package/src/bun/core/BrowserWindow.ts +396 -0
  46. package/src/bun/core/BuildConfig.ts +71 -0
  47. package/src/bun/core/ContextMenu.ts +75 -0
  48. package/src/bun/core/GpuWindow.ts +289 -0
  49. package/src/bun/core/Paths.ts +5 -0
  50. package/src/bun/core/Socket.ts +22 -0
  51. package/src/bun/core/Tray.ts +197 -0
  52. package/src/bun/core/Updater.ts +1131 -0
  53. package/src/bun/core/Utils.ts +487 -0
  54. package/src/bun/core/WGPUView.ts +167 -0
  55. package/src/bun/core/menuRoles.ts +181 -0
  56. package/src/bun/events/ApplicationEvents.ts +22 -0
  57. package/src/bun/events/event.ts +27 -0
  58. package/src/bun/events/eventEmitter.ts +45 -0
  59. package/src/bun/events/trayEvents.ts +11 -0
  60. package/src/bun/events/webviewEvents.ts +39 -0
  61. package/src/bun/events/windowEvents.ts +23 -0
  62. package/src/bun/index.ts +120 -0
  63. package/src/bun/preload/.generated/compiled.ts +2 -0
  64. package/src/bun/preload/build.ts +65 -0
  65. package/src/bun/preload/dragRegions.ts +41 -0
  66. package/src/bun/preload/encryption.ts +86 -0
  67. package/src/bun/preload/events.ts +171 -0
  68. package/src/bun/preload/globals.d.ts +45 -0
  69. package/src/bun/preload/index-sandboxed.ts +28 -0
  70. package/src/bun/preload/index.ts +77 -0
  71. package/src/bun/preload/internalRpc.ts +80 -0
  72. package/src/bun/preload/overlaySync.ts +107 -0
  73. package/src/bun/preload/webviewTag.ts +451 -0
  74. package/src/bun/preload/wgpuTag.ts +246 -0
  75. package/src/bun/proc/linux.md +43 -0
  76. package/src/bun/proc/native.ts +3253 -0
  77. package/src/bun/webGPU.ts +346 -0
  78. package/src/bun/webgpuAdapter.ts +3011 -0
  79. package/src/cli/bun.lockb +0 -0
  80. package/src/cli/index.ts +4653 -0
  81. package/src/cli/package-lock.json +81 -0
  82. package/src/cli/package.json +11 -0
  83. package/src/cli/templates/embedded.ts +2 -0
  84. package/src/core/build.zig +16 -0
  85. package/src/core/main.zig +3378 -0
  86. package/src/extractor/build.zig +22 -0
  87. package/src/installer/installer-template.ts +216 -0
  88. package/src/launcher/main.ts +221 -0
  89. package/src/native/build/libNativeWrapper.so +0 -0
  90. package/src/native/linux/build/nativeWrapper.o +0 -0
  91. package/src/native/linux/cef_loader.cpp +110 -0
  92. package/src/native/linux/cef_loader.h +28 -0
  93. package/src/native/linux/cef_process_helper_linux.cpp +160 -0
  94. package/src/native/linux/nativeWrapper.cpp +11768 -0
  95. package/src/native/macos/cef_process_helper_mac.cc +160 -0
  96. package/src/native/macos/nativeWrapper.mm +9172 -0
  97. package/src/native/shared/accelerator_parser.h +72 -0
  98. package/src/native/shared/app_paths.h +110 -0
  99. package/src/native/shared/asar.h +35 -0
  100. package/src/native/shared/cache_migration.h +244 -0
  101. package/src/native/shared/callbacks.h +57 -0
  102. package/src/native/shared/cef_response_filter.h +189 -0
  103. package/src/native/shared/chromium_flags.h +181 -0
  104. package/src/native/shared/config.h +66 -0
  105. package/src/native/shared/download_event.h +197 -0
  106. package/src/native/shared/ffi_helpers.h +139 -0
  107. package/src/native/shared/glob_match.h +59 -0
  108. package/src/native/shared/json_menu_parser.h +223 -0
  109. package/src/native/shared/mime_types.h +101 -0
  110. package/src/native/shared/navigation_rules.h +98 -0
  111. package/src/native/shared/partition_context.h +137 -0
  112. package/src/native/shared/pending_resize_queue.h +45 -0
  113. package/src/native/shared/permissions.h +118 -0
  114. package/src/native/shared/permissions_cef.h +74 -0
  115. package/src/native/shared/preload_script.h +71 -0
  116. package/src/native/shared/shutdown_guard.h +134 -0
  117. package/src/native/shared/thread_safe_map.h +138 -0
  118. package/src/native/shared/webview_storage.h +91 -0
  119. package/src/native/win/cef_process_helper_win.cpp +143 -0
  120. package/src/native/win/dcomp_compositor.h +352 -0
  121. package/src/native/win/nativeWrapper.cpp +12434 -0
  122. package/src/npmbin/index.js +34 -0
  123. package/src/shared/bun-version.ts +3 -0
  124. package/src/shared/cef-version.ts +5 -0
  125. package/src/shared/naming.test.ts +327 -0
  126. package/src/shared/naming.ts +188 -0
  127. package/src/shared/platform.ts +48 -0
  128. package/src/shared/rpc.ts +541 -0
  129. package/src/shared/sparkbun-version.ts +2 -0
  130. package/src/types/three.d.ts +1 -0
  131. package/tsconfig.json +31 -0
@@ -0,0 +1,1131 @@
1
+ import { join, dirname, resolve } from "path";
2
+ import { homedir } from "os";
3
+ import {
4
+ renameSync,
5
+ unlinkSync,
6
+ mkdirSync,
7
+ rmSync,
8
+ statSync,
9
+ readdirSync,
10
+ readFileSync,
11
+ writeFileSync,
12
+ } from "fs";
13
+ import { execSync } from "child_process";
14
+ import { OS as currentOS, ARCH as currentArch } from "../../shared/platform";
15
+ import { getPlatformPrefix, getTarballFileName } from "../../shared/naming";
16
+ import { quit } from "./Utils";
17
+
18
+ // Update status types for granular progress tracking
19
+ export type UpdateStatusType =
20
+ | "idle"
21
+ | "checking"
22
+ | "check-complete"
23
+ | "no-update"
24
+ | "update-available"
25
+ | "downloading"
26
+ | "download-starting"
27
+ | "checking-local-tar"
28
+ | "local-tar-found"
29
+ | "local-tar-missing"
30
+ | "fetching-patch"
31
+ | "patch-found"
32
+ | "patch-not-found"
33
+ | "downloading-patch"
34
+ | "applying-patch"
35
+ | "patch-applied"
36
+ | "patch-failed"
37
+ | "extracting-version"
38
+ | "patch-chain-complete"
39
+ | "downloading-full-bundle"
40
+ | "download-progress"
41
+ | "decompressing"
42
+ | "download-complete"
43
+ | "applying"
44
+ | "extracting"
45
+ | "replacing-app"
46
+ | "launching-new-version"
47
+ | "complete"
48
+ | "error";
49
+
50
+ export interface UpdateStatusDetails {
51
+ fromHash?: string;
52
+ toHash?: string;
53
+ currentHash?: string;
54
+ latestHash?: string;
55
+ patchNumber?: number;
56
+ totalPatchesApplied?: number;
57
+ progress?: number;
58
+ bytesDownloaded?: number;
59
+ totalBytes?: number;
60
+ usedPatchPath?: boolean;
61
+ errorMessage?: string;
62
+ url?: string;
63
+ exitCode?: number | null;
64
+ }
65
+
66
+ export interface UpdateStatusEntry {
67
+ status: UpdateStatusType;
68
+ message: string;
69
+ timestamp: number;
70
+ details?: UpdateStatusDetails;
71
+ }
72
+
73
+ // Status history and callback
74
+ const statusHistory: UpdateStatusEntry[] = [];
75
+ let onStatusChangeCallback: ((entry: UpdateStatusEntry) => void) | null = null;
76
+
77
+ function emitStatus(
78
+ status: UpdateStatusType,
79
+ message: string,
80
+ details?: UpdateStatusDetails,
81
+ ): void {
82
+ const entry: UpdateStatusEntry = {
83
+ status,
84
+ message,
85
+ timestamp: Date.now(),
86
+ details,
87
+ };
88
+ statusHistory.push(entry);
89
+ if (onStatusChangeCallback) {
90
+ onStatusChangeCallback(entry);
91
+ }
92
+ }
93
+
94
+ // setTimeout(async () => {
95
+ // console.log('killing')
96
+ // const { native } = await import('../proc/native');
97
+ // native.symbols.killApp();
98
+ // }, 1000)
99
+
100
+
101
+ // Cross-platform app data directory
102
+ function getAppDataDir(): string {
103
+ switch (currentOS) {
104
+ case "macos":
105
+ return join(homedir(), "Library", "Application Support");
106
+ case "win":
107
+ // Use LOCALAPPDATA to match extractor location
108
+ return process.env["LOCALAPPDATA"] || join(homedir(), "AppData", "Local");
109
+ case "linux":
110
+ // Use XDG_DATA_HOME or fallback to ~/.local/share to match extractor
111
+ return process.env["XDG_DATA_HOME"] || join(homedir(), ".local", "share");
112
+ default:
113
+ // Fallback to home directory with .config
114
+ return join(homedir(), ".config");
115
+ }
116
+ }
117
+
118
+ let localInfo: {
119
+ version: string;
120
+ hash: string;
121
+ baseUrl: string;
122
+ channel: string;
123
+ name: string;
124
+ identifier: string;
125
+ };
126
+
127
+ let updateInfo: {
128
+ version: string;
129
+ hash: string;
130
+ updateAvailable: boolean;
131
+ updateReady: boolean;
132
+ error: string;
133
+ };
134
+
135
+ function cleanupExtractionFolder(
136
+ extractionFolder: string,
137
+ keepTarHash: string,
138
+ ) {
139
+ const keepFile = `${keepTarHash}.tar`;
140
+ try {
141
+ const entries = readdirSync(extractionFolder);
142
+ for (const entry of entries) {
143
+ if (entry === keepFile) continue;
144
+ const fullPath = join(extractionFolder, entry);
145
+ try {
146
+ const s = statSync(fullPath);
147
+ if (s.isDirectory()) {
148
+ rmSync(fullPath, { recursive: true });
149
+ } else {
150
+ unlinkSync(fullPath);
151
+ }
152
+ } catch (e) {
153
+ // Best effort — file may be in use on Windows
154
+ }
155
+ }
156
+ } catch (e) {
157
+ // Ignore errors in cleanup
158
+ }
159
+ }
160
+
161
+ const Updater = {
162
+ updateInfo: () => {
163
+ return updateInfo;
164
+ },
165
+
166
+ // Status history and subscription methods
167
+ getStatusHistory: () => {
168
+ return [...statusHistory];
169
+ },
170
+
171
+ clearStatusHistory: () => {
172
+ statusHistory.length = 0;
173
+ },
174
+
175
+ onStatusChange: (callback: ((entry: UpdateStatusEntry) => void) | null) => {
176
+ onStatusChangeCallback = callback;
177
+ },
178
+
179
+ // todo: allow switching channels, by default will check the current channel
180
+ checkForUpdate: async () => {
181
+ emitStatus("checking", "Checking for updates...");
182
+ const localInfo = await Updater.getLocalInfo();
183
+
184
+ if (localInfo.channel === "dev") {
185
+ emitStatus("no-update", "Dev channel - updates disabled", {
186
+ currentHash: localInfo.hash,
187
+ });
188
+ return {
189
+ version: localInfo.version,
190
+ hash: localInfo.hash,
191
+ updateAvailable: false,
192
+ updateReady: false,
193
+ error: "",
194
+ };
195
+ }
196
+
197
+ const cacheBuster = Math.random().toString(36).substring(7);
198
+ const platformPrefix = getPlatformPrefix(
199
+ localInfo.channel,
200
+ currentOS,
201
+ currentArch,
202
+ );
203
+ const updateInfoUrl = `${localInfo.baseUrl.replace(/\/+$/, "")}/${platformPrefix}-update.json?${cacheBuster}`;
204
+
205
+ try {
206
+ const updateInfoResponse = await fetch(updateInfoUrl);
207
+
208
+ if (updateInfoResponse.ok) {
209
+ const responseText = await updateInfoResponse.text();
210
+ try {
211
+ updateInfo = JSON.parse(responseText);
212
+ } catch {
213
+ emitStatus("error", "Invalid update.json: failed to parse JSON", {
214
+ url: updateInfoUrl,
215
+ });
216
+ return {
217
+ version: "",
218
+ hash: "",
219
+ updateAvailable: false,
220
+ updateReady: false,
221
+ error: `Invalid update.json: failed to parse JSON`,
222
+ };
223
+ }
224
+
225
+ if (!updateInfo.hash) {
226
+ emitStatus("error", "Invalid update.json: missing hash", {
227
+ url: updateInfoUrl,
228
+ });
229
+ return {
230
+ version: "",
231
+ hash: "",
232
+ updateAvailable: false,
233
+ updateReady: false,
234
+ error: `Invalid update.json: missing hash`,
235
+ };
236
+ }
237
+
238
+ if (updateInfo.hash !== localInfo.hash) {
239
+ updateInfo.updateAvailable = true;
240
+ emitStatus(
241
+ "update-available",
242
+ `Update available: ${localInfo.hash.slice(0, 8)} → ${updateInfo.hash.slice(0, 8)}`,
243
+ {
244
+ currentHash: localInfo.hash,
245
+ latestHash: updateInfo.hash,
246
+ },
247
+ );
248
+ } else {
249
+ emitStatus("no-update", "Already on latest version", {
250
+ currentHash: localInfo.hash,
251
+ });
252
+ }
253
+ } else {
254
+ emitStatus(
255
+ "error",
256
+ `Failed to fetch update info (HTTP ${updateInfoResponse.status})`,
257
+ { url: updateInfoUrl },
258
+ );
259
+ return {
260
+ version: "",
261
+ hash: "",
262
+ updateAvailable: false,
263
+ updateReady: false,
264
+ error: `Failed to fetch update info from ${updateInfoUrl}`,
265
+ };
266
+ }
267
+ } catch (error) {
268
+ return {
269
+ version: "",
270
+ hash: "",
271
+ updateAvailable: false,
272
+ updateReady: false,
273
+ error: `Failed to fetch update info from ${updateInfoUrl}`,
274
+ };
275
+ }
276
+
277
+ return updateInfo;
278
+ },
279
+
280
+ downloadUpdate: async () => {
281
+ emitStatus("download-starting", "Starting update download...");
282
+ const appDataFolder = await Updater.appDataFolder();
283
+ await Updater.channelBucketUrl(); // Ensure localInfo is loaded
284
+ const appFileName = localInfo.name;
285
+
286
+ let currentHash = (await Updater.getLocalInfo()).hash;
287
+ let latestHash = (await Updater.checkForUpdate()).hash;
288
+
289
+ const extractionFolder = join(appDataFolder, "self-extraction");
290
+ if (!(await Bun.file(extractionFolder).exists())) {
291
+ mkdirSync(extractionFolder, { recursive: true });
292
+ }
293
+
294
+ let currentTarPath = join(extractionFolder, `${currentHash}.tar`);
295
+ const latestTarPath = join(extractionFolder, `${latestHash}.tar`);
296
+
297
+ const seenHashes: string[] = [];
298
+ let patchesApplied = 0;
299
+ let usedPatchPath = false;
300
+
301
+ if (!(await Bun.file(latestTarPath).exists())) {
302
+ emitStatus(
303
+ "checking-local-tar",
304
+ `Checking for local tar file: ${currentHash.slice(0, 8)}`,
305
+ { currentHash },
306
+ );
307
+
308
+ while (currentHash !== latestHash) {
309
+ seenHashes.push(currentHash);
310
+ const currentTar = Bun.file(currentTarPath);
311
+
312
+ if (!(await currentTar.exists())) {
313
+ // tar file of the current version not found
314
+ // so we can't patch it. We need the byte-for-byte tar file
315
+ // so break out and download the full version
316
+ emitStatus(
317
+ "local-tar-missing",
318
+ `Local tar not found for ${currentHash.slice(0, 8)}, will download full bundle`,
319
+ { currentHash },
320
+ );
321
+ break;
322
+ }
323
+
324
+ emitStatus(
325
+ "local-tar-found",
326
+ `Found local tar for ${currentHash.slice(0, 8)}`,
327
+ { currentHash },
328
+ );
329
+
330
+ // check if there's a patch file for it
331
+ const platformPrefix = getPlatformPrefix(
332
+ localInfo.channel,
333
+ currentOS,
334
+ currentArch,
335
+ );
336
+ const patchUrl = `${localInfo.baseUrl.replace(/\/+$/, "")}/${platformPrefix}-${currentHash}.patch`;
337
+ emitStatus(
338
+ "fetching-patch",
339
+ `Checking for patch: ${currentHash.slice(0, 8)}`,
340
+ { currentHash, url: patchUrl },
341
+ );
342
+
343
+ const patchResponse = await fetch(patchUrl);
344
+
345
+ if (!patchResponse.ok) {
346
+ // patch not found
347
+ emitStatus(
348
+ "patch-not-found",
349
+ `No patch available for ${currentHash.slice(0, 8)}, will download full bundle`,
350
+ { currentHash },
351
+ );
352
+ break;
353
+ }
354
+
355
+ emitStatus(
356
+ "patch-found",
357
+ `Patch found for ${currentHash.slice(0, 8)}`,
358
+ { currentHash },
359
+ );
360
+ emitStatus(
361
+ "downloading-patch",
362
+ `Downloading patch for ${currentHash.slice(0, 8)}...`,
363
+ { currentHash },
364
+ );
365
+
366
+ // The patch file's name is the hash of the "from" version
367
+ const patchFilePath = join(
368
+ appDataFolder,
369
+ "self-extraction",
370
+ `${currentHash}.patch`,
371
+ );
372
+ await Bun.write(patchFilePath, await patchResponse.arrayBuffer());
373
+ // patch it to a tmp name
374
+ const tmpPatchedTarFilePath = join(
375
+ appDataFolder,
376
+ "self-extraction",
377
+ `from-${currentHash}.tar`,
378
+ );
379
+
380
+ const bunBinDir = dirname(process.execPath);
381
+ const bspatchBinName = currentOS === "win" ? "bspatch.exe" : "bspatch";
382
+ const bspatchPath = join(bunBinDir, bspatchBinName);
383
+
384
+ emitStatus(
385
+ "applying-patch",
386
+ `Applying patch ${patchesApplied + 1} for ${currentHash.slice(0, 8)}...`,
387
+ {
388
+ currentHash,
389
+ patchNumber: patchesApplied + 1,
390
+ },
391
+ );
392
+
393
+ // Verify all files exist before invoking bspatch
394
+ if (!statSync(bspatchPath, { throwIfNoEntry: false })) {
395
+ emitStatus(
396
+ "patch-failed",
397
+ `bspatch binary not found at ${bspatchPath}`,
398
+ {
399
+ currentHash,
400
+ errorMessage: `bspatch not found: ${bspatchPath}`,
401
+ },
402
+ );
403
+ console.error("bspatch not found:", bspatchPath);
404
+ break;
405
+ }
406
+ if (!statSync(currentTarPath, { throwIfNoEntry: false })) {
407
+ emitStatus("patch-failed", `Old tar not found at ${currentTarPath}`, {
408
+ currentHash,
409
+ errorMessage: `old tar not found: ${currentTarPath}`,
410
+ });
411
+ console.error("old tar not found:", currentTarPath);
412
+ break;
413
+ }
414
+ if (!statSync(patchFilePath, { throwIfNoEntry: false })) {
415
+ emitStatus(
416
+ "patch-failed",
417
+ `Patch file not found at ${patchFilePath}`,
418
+ {
419
+ currentHash,
420
+ errorMessage: `patch not found: ${patchFilePath}`,
421
+ },
422
+ );
423
+ console.error("patch file not found:", patchFilePath);
424
+ break;
425
+ }
426
+
427
+ try {
428
+ const patchResult = Bun.spawnSync([
429
+ bspatchPath,
430
+ currentTarPath,
431
+ tmpPatchedTarFilePath,
432
+ patchFilePath,
433
+ ]);
434
+
435
+ if (patchResult.exitCode !== 0 || patchResult.success === false) {
436
+ const stderr = patchResult.stderr
437
+ ? patchResult.stderr.toString()
438
+ : "";
439
+ const stdout = patchResult.stdout
440
+ ? patchResult.stdout.toString()
441
+ : "";
442
+ if (updateInfo) {
443
+ updateInfo.error =
444
+ stderr ||
445
+ `bspatch failed with exit code ${patchResult.exitCode}`;
446
+ }
447
+ emitStatus(
448
+ "patch-failed",
449
+ `Patch application failed: ${stderr || `exit code ${patchResult.exitCode}`}`,
450
+ {
451
+ currentHash,
452
+ errorMessage: stderr || `exit code ${patchResult.exitCode}`,
453
+ },
454
+ );
455
+ console.error("bspatch failed", {
456
+ exitCode: patchResult.exitCode,
457
+ stdout,
458
+ stderr,
459
+ bspatchPath,
460
+ oldTar: currentTarPath,
461
+ newTar: tmpPatchedTarFilePath,
462
+ patch: patchFilePath,
463
+ });
464
+ break;
465
+ }
466
+ } catch (error) {
467
+ emitStatus(
468
+ "patch-failed",
469
+ `Patch threw exception: ${(error as Error).message}`,
470
+ {
471
+ currentHash,
472
+ errorMessage: (error as Error).message,
473
+ },
474
+ );
475
+ console.error("bspatch threw", error, { bspatchPath });
476
+ break;
477
+ }
478
+
479
+ patchesApplied++;
480
+ emitStatus(
481
+ "patch-applied",
482
+ `Patch ${patchesApplied} applied successfully`,
483
+ {
484
+ currentHash,
485
+ patchNumber: patchesApplied,
486
+ },
487
+ );
488
+
489
+ emitStatus(
490
+ "extracting-version",
491
+ "Extracting version info from patched tar...",
492
+ { currentHash },
493
+ );
494
+
495
+ let hashFilePath = "";
496
+
497
+ // Read the hash from the patched tar without full extraction:
498
+ // - macOS/Windows: Resources/version.json (inside the app bundle directory)
499
+ // - Linux: metadata.json (alongside the app bundle)
500
+ const resourcesDir = "Resources";
501
+ const patchedTarBytes = await Bun.file(
502
+ tmpPatchedTarFilePath,
503
+ ).arrayBuffer();
504
+ const patchedArchive = new Bun.Archive(patchedTarBytes);
505
+ const patchedFiles = await patchedArchive.files();
506
+
507
+ for (const [filePath] of patchedFiles) {
508
+ if (
509
+ filePath.endsWith(`${resourcesDir}/version.json`) ||
510
+ filePath.endsWith("metadata.json")
511
+ ) {
512
+ hashFilePath = filePath;
513
+ break;
514
+ }
515
+ }
516
+
517
+ if (!hashFilePath) {
518
+ emitStatus(
519
+ "error",
520
+ "Could not find version/metadata file in patched tar",
521
+ { currentHash },
522
+ );
523
+ console.error(
524
+ "Neither Resources/version.json nor metadata.json found in patched tar:",
525
+ tmpPatchedTarFilePath,
526
+ );
527
+ break;
528
+ }
529
+
530
+ const hashFile = patchedFiles.get(hashFilePath);
531
+ const hashFileJson = JSON.parse(await hashFile!.text());
532
+ const nextHash = hashFileJson.hash;
533
+
534
+ if (seenHashes.includes(nextHash)) {
535
+ emitStatus(
536
+ "error",
537
+ "Cyclical update detected, falling back to full download",
538
+ { currentHash: nextHash },
539
+ );
540
+ console.log("Warning: cyclical update detected");
541
+ break;
542
+ }
543
+
544
+ seenHashes.push(nextHash);
545
+
546
+ if (!nextHash) {
547
+ emitStatus(
548
+ "error",
549
+ "Could not determine next hash from patched tar",
550
+ { currentHash },
551
+ );
552
+ break;
553
+ }
554
+ // Sync the patched tar file to the new hash
555
+ const updatedTarPath = join(
556
+ appDataFolder,
557
+ "self-extraction",
558
+ `${nextHash}.tar`,
559
+ );
560
+ renameSync(tmpPatchedTarFilePath, updatedTarPath);
561
+
562
+ // delete the old tar file
563
+ unlinkSync(currentTarPath);
564
+ unlinkSync(patchFilePath);
565
+
566
+ currentHash = nextHash;
567
+ currentTarPath = join(
568
+ appDataFolder,
569
+ "self-extraction",
570
+ `${currentHash}.tar`,
571
+ );
572
+
573
+ emitStatus(
574
+ "patch-applied",
575
+ `Patched to ${nextHash.slice(0, 8)}, checking for more patches...`,
576
+ {
577
+ currentHash: nextHash,
578
+ toHash: latestHash,
579
+ totalPatchesApplied: patchesApplied,
580
+ },
581
+ );
582
+ // loop through applying patches until we reach the latest version
583
+ // if we get stuck then exit and just download the full latest version
584
+ }
585
+
586
+ // Check if patch chain completed successfully
587
+ if (currentHash === latestHash && patchesApplied > 0) {
588
+ usedPatchPath = true;
589
+ emitStatus(
590
+ "patch-chain-complete",
591
+ `Patch chain complete! Applied ${patchesApplied} patches`,
592
+ {
593
+ totalPatchesApplied: patchesApplied,
594
+ currentHash: latestHash,
595
+ usedPatchPath: true,
596
+ },
597
+ );
598
+ }
599
+
600
+ // If we weren't able to apply patches to the current version,
601
+ // then just download it and unpack it
602
+ if (currentHash !== latestHash) {
603
+ emitStatus(
604
+ "downloading-full-bundle",
605
+ "Downloading full update bundle...",
606
+ {
607
+ currentHash,
608
+ latestHash,
609
+ usedPatchPath: false,
610
+ },
611
+ );
612
+
613
+ const cacheBuster = Math.random().toString(36).substring(7);
614
+ const platformPrefix = getPlatformPrefix(
615
+ localInfo.channel,
616
+ currentOS,
617
+ currentArch,
618
+ );
619
+ const tarballName = getTarballFileName(appFileName, currentOS);
620
+ const urlToLatestTarball = `${localInfo.baseUrl.replace(/\/+$/, "")}/${platformPrefix}-${tarballName}`;
621
+ const prevVersionCompressedTarballPath = join(
622
+ appDataFolder,
623
+ "self-extraction",
624
+ "latest.tar.gz",
625
+ );
626
+
627
+ emitStatus("download-progress", `Fetching ${tarballName}...`, {
628
+ url: urlToLatestTarball,
629
+ });
630
+ const response = await fetch(urlToLatestTarball + `?${cacheBuster}`);
631
+
632
+ if (response.ok && response.body) {
633
+ const contentLength = response.headers.get("content-length");
634
+ const totalBytes = contentLength
635
+ ? parseInt(contentLength, 10)
636
+ : undefined;
637
+ let bytesDownloaded = 0;
638
+
639
+ const reader = response.body.getReader();
640
+ const writer = Bun.file(prevVersionCompressedTarballPath).writer();
641
+
642
+ while (true) {
643
+ const { done, value } = await reader.read();
644
+ if (done) break;
645
+ await writer.write(value);
646
+ bytesDownloaded += value.length;
647
+
648
+ // Emit progress every ~500KB or so
649
+ if (bytesDownloaded % 500000 < value.length) {
650
+ emitStatus(
651
+ "download-progress",
652
+ `Downloading: ${(bytesDownloaded / 1024 / 1024).toFixed(1)} MB`,
653
+ {
654
+ bytesDownloaded,
655
+ totalBytes,
656
+ progress: totalBytes
657
+ ? Math.round((bytesDownloaded / totalBytes) * 100)
658
+ : undefined,
659
+ },
660
+ );
661
+ }
662
+ }
663
+ await writer.flush();
664
+ writer.end();
665
+
666
+ emitStatus(
667
+ "download-progress",
668
+ `Download complete: ${(bytesDownloaded / 1024 / 1024).toFixed(1)} MB`,
669
+ {
670
+ bytesDownloaded,
671
+ totalBytes,
672
+ progress: 100,
673
+ },
674
+ );
675
+ } else {
676
+ emitStatus("error", `Failed to download: ${urlToLatestTarball}`, {
677
+ url: urlToLatestTarball,
678
+ });
679
+ console.log("latest version not found at: ", urlToLatestTarball);
680
+ }
681
+
682
+ emitStatus("decompressing", "Decompressing update bundle...");
683
+ try {
684
+ const compressed = readFileSync(prevVersionCompressedTarballPath);
685
+ const decompressed = Bun.gunzipSync(compressed);
686
+ writeFileSync(latestTarPath, decompressed);
687
+ emitStatus("decompressing", "Decompression complete");
688
+ } catch (err) {
689
+ updateInfo.error = `Decompression failed: ${err}`;
690
+ emitStatus("error", updateInfo.error);
691
+ console.error("Decompression failed:", err);
692
+ }
693
+
694
+ unlinkSync(prevVersionCompressedTarballPath);
695
+ }
696
+ }
697
+
698
+ // Note: Bun.file().exists() caches the result, so we nee d an new instance of Bun.file() here
699
+ // to check again
700
+ if (await Bun.file(latestTarPath).exists()) {
701
+ // download patch for this version, apply it.
702
+ // check for patch from that tar and apply it, until it matches the latest version
703
+ // as a fallback it should just download and unpack the latest version
704
+ updateInfo.updateReady = true;
705
+ emitStatus(
706
+ "download-complete",
707
+ `Update ready to install (used ${usedPatchPath ? "patch" : "full download"} path)`,
708
+ {
709
+ latestHash,
710
+ usedPatchPath,
711
+ totalPatchesApplied: patchesApplied,
712
+ },
713
+ );
714
+ } else {
715
+ updateInfo.error = "Failed to download latest version";
716
+ emitStatus("error", "Failed to download latest version", { latestHash });
717
+ }
718
+
719
+ // Clean up stale files in the extraction folder (old tars, patches, backups, etc.)
720
+ cleanupExtractionFolder(extractionFolder, latestHash);
721
+ },
722
+
723
+ applyUpdate: async () => {
724
+ if (updateInfo?.updateReady) {
725
+ emitStatus("applying", "Starting update installation...");
726
+ const appDataFolder = await Updater.appDataFolder();
727
+ const extractionFolder = join(appDataFolder, "self-extraction");
728
+ if (!(await Bun.file(extractionFolder).exists())) {
729
+ mkdirSync(extractionFolder, { recursive: true });
730
+ }
731
+
732
+ let latestHash = (await Updater.checkForUpdate()).hash;
733
+ const latestTarPath = join(extractionFolder, `${latestHash}.tar`);
734
+
735
+ let appBundleSubpath: string = "";
736
+
737
+ if (await Bun.file(latestTarPath).exists()) {
738
+ emitStatus(
739
+ "extracting",
740
+ `Extracting update to ${latestHash.slice(0, 8)}...`,
741
+ { latestHash },
742
+ );
743
+
744
+ // Windows needs a temporary directory to avoid file locking issues
745
+ const extractionDir =
746
+ currentOS === "win"
747
+ ? join(extractionFolder, `temp-${latestHash}`)
748
+ : extractionFolder;
749
+
750
+ if (currentOS === "win") {
751
+ mkdirSync(extractionDir, { recursive: true });
752
+ }
753
+
754
+ const latestTarBytes = await Bun.file(latestTarPath).arrayBuffer();
755
+ const latestArchive = new Bun.Archive(latestTarBytes);
756
+ await latestArchive.extract(extractionDir);
757
+
758
+ if (currentOS === "macos") {
759
+ // Find the .app bundle by scanning extracted directory
760
+ const extractedFiles = readdirSync(extractionDir);
761
+ for (const file of extractedFiles) {
762
+ if (file.endsWith('.app')) {
763
+ appBundleSubpath = file + "/";
764
+ break;
765
+ }
766
+ }
767
+ } else {
768
+ appBundleSubpath = "./";
769
+ }
770
+
771
+ console.log(
772
+ `Tar extraction completed. Found appBundleSubpath: ${appBundleSubpath}`,
773
+ );
774
+
775
+ if (!appBundleSubpath) {
776
+ console.error("Failed to find app in tarball");
777
+ return;
778
+ }
779
+
780
+ // Note: resolve here removes the extra trailing / that the tar file adds
781
+ const extractedAppPath = resolve(join(extractionDir, appBundleSubpath));
782
+
783
+ // Platform-specific path handling
784
+ let newAppBundlePath: string;
785
+ if (currentOS === "linux") {
786
+ // On Linux, the tarball contains a directory bundle
787
+ // Find the actual extracted app directory name instead of guessing
788
+ const extractedFiles = readdirSync(extractionDir);
789
+ const appBundleDir = extractedFiles.find(file => {
790
+ const filePath = join(extractionDir, file);
791
+ return statSync(filePath).isDirectory() && !file.endsWith('.tar');
792
+ });
793
+
794
+ if (!appBundleDir) {
795
+ console.error("Could not find app bundle directory in extraction");
796
+ return;
797
+ }
798
+
799
+ newAppBundlePath = join(extractionDir, appBundleDir);
800
+
801
+ // Verify the app bundle directory exists
802
+ const bundleStats = statSync(newAppBundlePath, { throwIfNoEntry: false });
803
+ if (!bundleStats || !bundleStats.isDirectory()) {
804
+ console.error(`App bundle directory not found at: ${newAppBundlePath}`);
805
+ console.log("Contents of extraction directory:");
806
+ try {
807
+ const files = readdirSync(extractionDir);
808
+ for (const file of files) {
809
+ console.log(` - ${file}`);
810
+ // Also list contents of subdirectories
811
+ const subPath = join(extractionDir, file);
812
+ if (statSync(subPath).isDirectory()) {
813
+ const subFiles = readdirSync(subPath);
814
+ for (const subFile of subFiles) {
815
+ console.log(` - ${subFile}`);
816
+ }
817
+ }
818
+ }
819
+ } catch (e) {
820
+ console.log("Could not list directory contents:", e);
821
+ }
822
+ return;
823
+ }
824
+ } else if (currentOS === "win") {
825
+ // On Windows, the actual app is inside a subdirectory.
826
+ // version.json's `name` field already contains the formatted app
827
+ // file name (e.g. "MyApp-canary" for non-stable, "MyApp" for stable),
828
+ // so don't re-apply getAppFileName or it doubles the channel suffix.
829
+ newAppBundlePath = join(extractionDir, localInfo.name);
830
+
831
+ // Verify the extracted app exists
832
+ if (!statSync(newAppBundlePath, { throwIfNoEntry: false })) {
833
+ console.error(`Extracted app not found at: ${newAppBundlePath}`);
834
+ console.log("Contents of extraction directory:");
835
+ try {
836
+ const files = readdirSync(extractionDir);
837
+ for (const file of files) {
838
+ console.log(` - ${file}`);
839
+ }
840
+ } catch (e) {
841
+ console.log("Could not list directory contents:", e);
842
+ }
843
+ return;
844
+ }
845
+ } else {
846
+ // On macOS, use the extracted app path directly
847
+ newAppBundlePath = extractedAppPath;
848
+ }
849
+ // Platform-specific app path calculation
850
+ let runningAppBundlePath: string;
851
+ const appDataFolder = await Updater.appDataFolder();
852
+
853
+ if (currentOS === "macos") {
854
+ // On macOS, executable is at Contents/MacOS/binary inside .app bundle
855
+ runningAppBundlePath = resolve(dirname(process.execPath), "..", "..");
856
+ } else if (currentOS === "linux" || currentOS === "win") {
857
+ // On Linux and Windows, use fixed 'app' folder to match extractor
858
+ runningAppBundlePath = join(appDataFolder, "app");
859
+ } else {
860
+ throw new Error(`Unsupported platform: ${currentOS}`);
861
+ }
862
+ try {
863
+ emitStatus("replacing-app", "Removing old version...");
864
+
865
+ if (currentOS === "macos") {
866
+ // Remove existing app before installing the new one
867
+ if (statSync(runningAppBundlePath, { throwIfNoEntry: false })) {
868
+ rmSync(runningAppBundlePath, { recursive: true });
869
+ }
870
+
871
+ emitStatus("replacing-app", "Installing new version...");
872
+ // Move new app to running location
873
+ renameSync(newAppBundlePath, runningAppBundlePath);
874
+
875
+ // Remove quarantine extended attributes to prevent "damaged" error
876
+ // The inner bundle is already signed/notarized, but macOS applies
877
+ // quarantine attributes when extracting from a downloaded archive
878
+ try {
879
+ execSync(
880
+ `xattr -r -d com.apple.quarantine "${runningAppBundlePath}"`,
881
+ { stdio: "ignore" },
882
+ );
883
+ } catch (e) {
884
+ // Ignore errors - attribute may not exist
885
+ }
886
+ } else if (currentOS === "linux") {
887
+ // On Linux, we now have directory bundles instead of AppImage files
888
+ // The app is stored in {appDataFolder}/app/
889
+ const appBundleDir = join(appDataFolder, "app");
890
+
891
+ // Remove existing app directory if it exists
892
+ if (statSync(appBundleDir, { throwIfNoEntry: false })) {
893
+ rmSync(appBundleDir, { recursive: true });
894
+ }
895
+
896
+ // Move new app bundle directory to app location
897
+ renameSync(newAppBundlePath, appBundleDir);
898
+
899
+ // Ensure launcher binary is executable
900
+ const launcherPath = join(appBundleDir, "bin", "launcher");
901
+ if (statSync(launcherPath, { throwIfNoEntry: false })) {
902
+ execSync(`chmod +x "${launcherPath}"`);
903
+ }
904
+
905
+ // Also ensure other binaries are executable
906
+ const bunPath = join(appBundleDir, "bin", "bun");
907
+ if (statSync(bunPath, { throwIfNoEntry: false })) {
908
+ execSync(`chmod +x "${bunPath}"`);
909
+ }
910
+ }
911
+
912
+ // Clean up stale files in extraction folder
913
+ if (currentOS !== "win") {
914
+ cleanupExtractionFolder(extractionFolder, latestHash);
915
+ }
916
+
917
+ if (currentOS === "win") {
918
+ // On Windows, files are locked while in use, so we need a helper script
919
+ // that runs after the app exits to do the replacement
920
+ const parentDir = dirname(runningAppBundlePath);
921
+ const updateScriptPath = join(parentDir, "update.bat");
922
+ const launcherPath = join(
923
+ runningAppBundlePath,
924
+ "bin",
925
+ "launcher.exe",
926
+ );
927
+
928
+ // Convert paths to Windows format
929
+ const runningAppWin = runningAppBundlePath.replace(/\//g, "\\");
930
+ const newAppWin = newAppBundlePath.replace(/\//g, "\\");
931
+ const extractionDirWin = extractionDir.replace(/\//g, "\\");
932
+ const launcherPathWin = launcherPath.replace(/\//g, "\\");
933
+
934
+ // Create a batch script that will:
935
+ // 1. Wait for the current app and its helper processes to exit
936
+ // 2. Remove current app folder (with retries — CEF helpers may briefly
937
+ // keep libcef.dll locked after launcher.exe exits)
938
+ // 3. Move new app to current location (only if old folder is fully gone,
939
+ // otherwise `move` would put it inside as a subdirectory)
940
+ // 4. Launch the new app
941
+ // 5. Clean up
942
+ const updateScript = `@echo off
943
+ setlocal
944
+
945
+ :: Wait for the app and any CEF helper processes to fully exit.
946
+ :: launcher.exe spawns bun.exe which spawns "bun Helper*.exe" processes that
947
+ :: keep libcef.dll locked; if we proceed too early, rmdir partially fails.
948
+ :waitloop
949
+ tasklist /FI "IMAGENAME eq launcher.exe" 2>NUL | find /I /N "launcher.exe">NUL && goto waitsleep
950
+ tasklist /FI "IMAGENAME eq bun.exe" 2>NUL | find /I /N "bun.exe">NUL && goto waitsleep
951
+ tasklist /FI "IMAGENAME eq bun Helper.exe" 2>NUL | find /I /N "bun Helper.exe">NUL && goto waitsleep
952
+ tasklist 2>NUL | find /I "bun Helper">NUL && goto waitsleep
953
+ goto waitdone
954
+ :waitsleep
955
+ timeout /t 1 /nobreak >nul
956
+ goto waitloop
957
+ :waitdone
958
+
959
+ :: Small extra delay to ensure all file handles are released
960
+ timeout /t 2 /nobreak >nul
961
+
962
+ :: Remove current app folder, retrying if rmdir fails (locked files etc.)
963
+ set rmRetry=0
964
+ :rmloop
965
+ if not exist "${runningAppWin}" goto rmdone
966
+ rmdir /s /q "${runningAppWin}" 2>nul
967
+ if not exist "${runningAppWin}" goto rmdone
968
+ set /a rmRetry=rmRetry+1
969
+ if %rmRetry% GEQ 10 goto rmfailed
970
+ timeout /t 2 /nobreak >nul
971
+ goto rmloop
972
+ :rmfailed
973
+ echo Update failed: could not remove "${runningAppWin}" after retries.
974
+ echo Files may still be locked by a helper process.
975
+ pause
976
+ exit /b 1
977
+ :rmdone
978
+
979
+ :: Move new app to current location (safe now that destination is gone)
980
+ move "${newAppWin}" "${runningAppWin}"
981
+ if not exist "${launcherPathWin}" (
982
+ echo Update failed: launcher not found at "${launcherPathWin}" after move.
983
+ pause
984
+ exit /b 1
985
+ )
986
+
987
+ :: Clean up extraction directory
988
+ rmdir /s /q "${extractionDirWin}" 2>nul
989
+
990
+ :: Launch the new app
991
+ start "" "${launcherPathWin}"
992
+
993
+ :: Clean up scheduled tasks starting with SparkBunUpdate_
994
+ for /f "tokens=1" %%t in ('schtasks /query /fo list ^| findstr /i "SparkBunUpdate_"') do (
995
+ schtasks /delete /tn "%%t" /f >nul 2>&1
996
+ )
997
+
998
+ :: Delete this update script after a short delay
999
+ ping -n 2 127.0.0.1 >nul
1000
+ del "%~f0"
1001
+ `;
1002
+
1003
+ await Bun.write(updateScriptPath, updateScript);
1004
+
1005
+ // Use Windows Task Scheduler to run the update script independently
1006
+ // This ensures the script runs even after the app exits
1007
+ const scriptPathWin = updateScriptPath.replace(/\//g, "\\");
1008
+ const taskName = `SparkBunUpdate_${Date.now()}`;
1009
+
1010
+ // Create a scheduled task that runs immediately and deletes itself
1011
+ execSync(
1012
+ `schtasks /create /tn "${taskName}" /tr "cmd /c \\"${scriptPathWin}\\"" /sc once /st 00:00 /f`,
1013
+ { stdio: "ignore" },
1014
+ );
1015
+ execSync(`schtasks /run /tn "${taskName}"`, { stdio: "ignore" });
1016
+ // The task will be cleaned up by Windows after it runs, or we delete it in the batch script
1017
+
1018
+ // Use quit() for graceful shutdown - this closes all windows and processes
1019
+ quit();
1020
+ }
1021
+ } catch (error) {
1022
+ emitStatus(
1023
+ "error",
1024
+ `Failed to replace app: ${(error as Error).message}`,
1025
+ {
1026
+ errorMessage: (error as Error).message,
1027
+ },
1028
+ );
1029
+ console.error("Failed to replace app with new version", error);
1030
+ return;
1031
+ }
1032
+
1033
+ emitStatus("launching-new-version", "Launching updated version...");
1034
+
1035
+ // Cross-platform app launch (Windows is handled above with its own update script)
1036
+ if (currentOS === "macos") {
1037
+ // Wait for the current process to fully exit before relaunching.
1038
+ // macOS 'open' on an already-running app just activates the existing
1039
+ // instance instead of launching a new one, so we must ensure the
1040
+ // current process has exited first. The detached shell survives our
1041
+ // exit and polls until the process is gone.
1042
+ const pid = process.pid;
1043
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1044
+ Bun.spawn(
1045
+ [
1046
+ "sh",
1047
+ "-c",
1048
+ `while kill -0 ${pid} 2>/dev/null; do sleep 0.5; done; sleep 1; open "${runningAppBundlePath}"`,
1049
+ ],
1050
+ {
1051
+ detached: true,
1052
+ stdio: ["ignore", "ignore", "ignore"],
1053
+ } as any,
1054
+ );
1055
+ } else if (currentOS === "linux") {
1056
+ // On Linux, launch the launcher binary inside the app directory
1057
+ const launcherPath = join(runningAppBundlePath, "bin", "launcher");
1058
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1059
+ Bun.spawn(["sh", "-c", `"${launcherPath}" &`], {
1060
+ detached: true,
1061
+ } as any);
1062
+ }
1063
+
1064
+ emitStatus("complete", "Update complete, restarting application...");
1065
+ // Use quit() for graceful shutdown
1066
+ quit();
1067
+ }
1068
+ }
1069
+ },
1070
+
1071
+ channelBucketUrl: async () => {
1072
+ await Updater.getLocalInfo();
1073
+ // With flat prefix-based naming, channelBucketUrl is just the baseUrl
1074
+ // Users can also use Updater.localInfo.baseUrl() directly
1075
+ return localInfo.baseUrl;
1076
+ },
1077
+
1078
+ appDataFolder: async () => {
1079
+ await Updater.getLocalInfo();
1080
+ // Use identifier + channel for the app data folder
1081
+ // e.g., ~/Library/Application Support/sh.blackboard.myapp/canary/
1082
+ const appDataFolder = join(
1083
+ getAppDataDir(),
1084
+ localInfo.identifier,
1085
+ localInfo.channel,
1086
+ );
1087
+
1088
+ return appDataFolder;
1089
+ },
1090
+
1091
+ // TODO: consider moving this from "Updater.localInfo" to "BuildVars"
1092
+ localInfo: {
1093
+ version: async () => {
1094
+ return (await Updater.getLocalInfo()).version;
1095
+ },
1096
+ hash: async () => {
1097
+ return (await Updater.getLocalInfo()).hash;
1098
+ },
1099
+ channel: async () => {
1100
+ return (await Updater.getLocalInfo()).channel;
1101
+ },
1102
+ baseUrl: async () => {
1103
+ return (await Updater.getLocalInfo()).baseUrl;
1104
+ },
1105
+ },
1106
+
1107
+ getLocalInfo: async () => {
1108
+ if (localInfo) {
1109
+ return localInfo;
1110
+ }
1111
+
1112
+ try {
1113
+ const resourcesDir = "Resources"; // Always use capitalized Resources
1114
+ localInfo = await Bun.file(`../${resourcesDir}/version.json`).json();
1115
+ return localInfo;
1116
+ } catch (error) {
1117
+ console.error("Failed to read version.json", error);
1118
+ localInfo = { identifier: "", channel: "", version: "", hash: "", baseUrl: "", name: "" };
1119
+ return localInfo;
1120
+ }
1121
+ },
1122
+ getLocallocalInfo: async () => {
1123
+ console.error(
1124
+ "[SparkBun] Updater.getLocallocalInfo() is deprecated. Use Updater.getLocalInfo() instead.",
1125
+ );
1126
+
1127
+ return Updater.getLocalInfo();
1128
+ },
1129
+ };
1130
+
1131
+ export { Updater };