electrobun 0.0.19-beta.13 → 0.0.19-beta.130

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 (100) hide show
  1. package/BUILD.md +90 -0
  2. package/README.md +1 -1
  3. package/bin/electrobun.cjs +2 -9
  4. package/debug.js +5 -0
  5. package/dist/api/browser/builtinrpcSchema.ts +19 -0
  6. package/dist/api/browser/index.ts +409 -0
  7. package/dist/api/browser/rpc/webview.ts +79 -0
  8. package/dist/api/browser/stylesAndElements.ts +3 -0
  9. package/dist/api/browser/webviewtag.ts +586 -0
  10. package/dist/api/bun/ElectrobunConfig.ts +171 -0
  11. package/dist/api/bun/core/ApplicationMenu.ts +66 -0
  12. package/dist/api/bun/core/BrowserView.ts +349 -0
  13. package/dist/api/bun/core/BrowserWindow.ts +195 -0
  14. package/dist/api/bun/core/ContextMenu.ts +67 -0
  15. package/dist/api/bun/core/Paths.ts +5 -0
  16. package/dist/api/bun/core/Socket.ts +181 -0
  17. package/dist/api/bun/core/Tray.ts +121 -0
  18. package/dist/api/bun/core/Updater.ts +681 -0
  19. package/dist/api/bun/core/Utils.ts +48 -0
  20. package/dist/api/bun/events/ApplicationEvents.ts +14 -0
  21. package/dist/api/bun/events/event.ts +29 -0
  22. package/dist/api/bun/events/eventEmitter.ts +45 -0
  23. package/dist/api/bun/events/trayEvents.ts +9 -0
  24. package/dist/api/bun/events/webviewEvents.ts +16 -0
  25. package/dist/api/bun/events/windowEvents.ts +12 -0
  26. package/dist/api/bun/index.ts +47 -0
  27. package/dist/api/bun/proc/linux.md +43 -0
  28. package/dist/api/bun/proc/native.ts +1322 -0
  29. package/dist/api/shared/platform.ts +48 -0
  30. package/dist/main.js +54 -0
  31. package/package.json +11 -6
  32. package/src/cli/index.ts +1353 -239
  33. package/templates/hello-world/README.md +57 -0
  34. package/templates/hello-world/bun.lock +225 -0
  35. package/templates/hello-world/electrobun.config.ts +28 -0
  36. package/templates/hello-world/package.json +16 -0
  37. package/templates/hello-world/src/bun/index.ts +15 -0
  38. package/templates/hello-world/src/mainview/index.css +124 -0
  39. package/templates/hello-world/src/mainview/index.html +46 -0
  40. package/templates/hello-world/src/mainview/index.ts +1 -0
  41. package/templates/interactive-playground/README.md +26 -0
  42. package/templates/interactive-playground/assets/tray-icon.png +0 -0
  43. package/templates/interactive-playground/electrobun.config.ts +36 -0
  44. package/templates/interactive-playground/package-lock.json +36 -0
  45. package/templates/interactive-playground/package.json +15 -0
  46. package/templates/interactive-playground/src/bun/demos/files.ts +70 -0
  47. package/templates/interactive-playground/src/bun/demos/menus.ts +139 -0
  48. package/templates/interactive-playground/src/bun/demos/rpc.ts +83 -0
  49. package/templates/interactive-playground/src/bun/demos/system.ts +72 -0
  50. package/templates/interactive-playground/src/bun/demos/updates.ts +105 -0
  51. package/templates/interactive-playground/src/bun/demos/windows.ts +90 -0
  52. package/templates/interactive-playground/src/bun/index.ts +124 -0
  53. package/templates/interactive-playground/src/bun/types/rpc.ts +109 -0
  54. package/templates/interactive-playground/src/mainview/components/EventLog.ts +107 -0
  55. package/templates/interactive-playground/src/mainview/components/Sidebar.ts +65 -0
  56. package/templates/interactive-playground/src/mainview/components/Toast.ts +57 -0
  57. package/templates/interactive-playground/src/mainview/demos/FileDemo.ts +211 -0
  58. package/templates/interactive-playground/src/mainview/demos/MenuDemo.ts +102 -0
  59. package/templates/interactive-playground/src/mainview/demos/RPCDemo.ts +229 -0
  60. package/templates/interactive-playground/src/mainview/demos/TrayDemo.ts +132 -0
  61. package/templates/interactive-playground/src/mainview/demos/WebViewDemo.ts +411 -0
  62. package/templates/interactive-playground/src/mainview/demos/WindowDemo.ts +207 -0
  63. package/templates/interactive-playground/src/mainview/index.css +538 -0
  64. package/templates/interactive-playground/src/mainview/index.html +103 -0
  65. package/templates/interactive-playground/src/mainview/index.ts +238 -0
  66. package/templates/multitab-browser/README.md +34 -0
  67. package/templates/multitab-browser/bun.lock +224 -0
  68. package/templates/multitab-browser/electrobun.config.ts +32 -0
  69. package/templates/multitab-browser/package-lock.json +20 -0
  70. package/templates/multitab-browser/package.json +12 -0
  71. package/templates/multitab-browser/src/bun/index.ts +144 -0
  72. package/templates/multitab-browser/src/bun/tabManager.ts +200 -0
  73. package/templates/multitab-browser/src/bun/types/rpc.ts +78 -0
  74. package/templates/multitab-browser/src/mainview/index.css +487 -0
  75. package/templates/multitab-browser/src/mainview/index.html +94 -0
  76. package/templates/multitab-browser/src/mainview/index.ts +630 -0
  77. package/templates/photo-booth/README.md +108 -0
  78. package/templates/photo-booth/bun.lock +239 -0
  79. package/templates/photo-booth/electrobun.config.ts +28 -0
  80. package/templates/photo-booth/package.json +16 -0
  81. package/templates/photo-booth/src/bun/index.ts +92 -0
  82. package/templates/photo-booth/src/mainview/index.css +465 -0
  83. package/templates/photo-booth/src/mainview/index.html +124 -0
  84. package/templates/photo-booth/src/mainview/index.ts +499 -0
  85. package/tests/bun.lock +14 -0
  86. package/tests/electrobun.config.ts +45 -0
  87. package/tests/package-lock.json +36 -0
  88. package/tests/package.json +13 -0
  89. package/tests/src/bun/index.ts +100 -0
  90. package/tests/src/bun/test-runner.ts +508 -0
  91. package/tests/src/mainview/index.html +110 -0
  92. package/tests/src/mainview/index.ts +458 -0
  93. package/tests/src/mainview/styles/main.css +451 -0
  94. package/tests/src/testviews/tray-test.html +57 -0
  95. package/tests/src/testviews/webview-mask.html +114 -0
  96. package/tests/src/testviews/webview-navigation.html +36 -0
  97. package/tests/src/testviews/window-create.html +17 -0
  98. package/tests/src/testviews/window-events.html +29 -0
  99. package/tests/src/testviews/window-focus.html +37 -0
  100. package/tests/src/webviewtag/index.ts +11 -0
@@ -0,0 +1,681 @@
1
+ import { join, dirname, resolve, basename, relative } from "path";
2
+ import { homedir } from "os";
3
+ import { renameSync, unlinkSync, mkdirSync, rmdirSync, statSync, readdirSync, cpSync } from "fs";
4
+ import { execSync } from "child_process";
5
+ import tar from "tar";
6
+ import { ZstdInit } from "@oneidentity/zstd-js/wasm";
7
+ import { OS as currentOS, ARCH as currentArch } from '../../shared/platform';
8
+ import { native } from '../proc/native';
9
+
10
+ // setTimeout(async () => {
11
+ // console.log('killing')
12
+ // const { native } = await import('../proc/native');
13
+ // native.symbols.killApp();
14
+ // }, 1000)
15
+
16
+ // Create or update run.sh launcher script for Linux
17
+ async function createLinuxLauncherScript(appDir: string): Promise<void> {
18
+ const parentDir = dirname(appDir);
19
+ const launcherPath = join(parentDir, "run.sh");
20
+
21
+ const launcherContent = `#!/bin/bash
22
+ # Electrobun App Launcher
23
+ # This script sets up the environment and launches the app
24
+
25
+ # Get the directory where this script is located
26
+ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
27
+ APP_DIR="$SCRIPT_DIR/app"
28
+
29
+ cd "$APP_DIR/bin"
30
+ export LD_LIBRARY_PATH=".:$LD_LIBRARY_PATH"
31
+
32
+ # Force X11 backend for compatibility
33
+ export GDK_BACKEND=x11
34
+
35
+ # Check if CEF libraries exist and set LD_PRELOAD
36
+ if [ -f "./libcef.so" ] || [ -f "./libvk_swiftshader.so" ]; then
37
+ CEF_LIBS=""
38
+ [ -f "./libcef.so" ] && CEF_LIBS="./libcef.so"
39
+ if [ -f "./libvk_swiftshader.so" ]; then
40
+ if [ -n "$CEF_LIBS" ]; then
41
+ CEF_LIBS="$CEF_LIBS:./libvk_swiftshader.so"
42
+ else
43
+ CEF_LIBS="./libvk_swiftshader.so"
44
+ fi
45
+ fi
46
+ export LD_PRELOAD="$CEF_LIBS"
47
+ fi
48
+
49
+ exec ./launcher "$@"
50
+ `;
51
+
52
+ await Bun.write(launcherPath, launcherContent);
53
+
54
+ // Make it executable
55
+ execSync(`chmod +x "${launcherPath}"`);
56
+
57
+ console.log(`Created/updated Linux launcher script: ${launcherPath}`);
58
+ }
59
+
60
+ // Cross-platform app data directory
61
+ function getAppDataDir(): string {
62
+ switch (currentOS) {
63
+ case 'macos':
64
+ return join(homedir(), "Library", "Application Support");
65
+ case 'win':
66
+ // Use LOCALAPPDATA to match extractor location
67
+ return process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local");
68
+ case 'linux':
69
+ // Use XDG_DATA_HOME or fallback to ~/.local/share to match extractor
70
+ return process.env.XDG_DATA_HOME || join(homedir(), ".local", "share");
71
+ default:
72
+ // Fallback to home directory with .config
73
+ return join(homedir(), ".config");
74
+ }
75
+ }
76
+
77
+ // todo (yoav): share type with cli
78
+ let localInfo: {
79
+ version: string;
80
+ hash: string;
81
+ bucketUrl: string;
82
+ channel: string;
83
+ name: string;
84
+ identifier: string;
85
+ };
86
+
87
+ let updateInfo: {
88
+ version: string;
89
+ hash: string;
90
+ updateAvailable: boolean;
91
+ updateReady: boolean;
92
+ error: string;
93
+ };
94
+
95
+ const Updater = {
96
+ // workaround for some weird state stuff in this old version of bun
97
+ // todo: revisit after updating to the latest bun
98
+ updateInfo: () => {
99
+ return updateInfo;
100
+ },
101
+ // todo: allow switching channels, by default will check the current channel
102
+ checkForUpdate: async () => {
103
+ const localInfo = await Updater.getLocallocalInfo();
104
+
105
+ if (localInfo.channel === "dev") {
106
+ return {
107
+ version: localInfo.version,
108
+ hash: localInfo.hash,
109
+ updateAvailable: false,
110
+ updateReady: false,
111
+ error: "",
112
+ };
113
+ }
114
+
115
+ const channelBucketUrl = await Updater.channelBucketUrl();
116
+ const cacheBuster = Math.random().toString(36).substring(7);
117
+ const platformFolder = `${localInfo.channel}-${currentOS}-${currentArch}`;
118
+ const updateInfoUrl = join(localInfo.bucketUrl, platformFolder, `update.json?${cacheBuster}`);
119
+
120
+ try {
121
+ const updateInfoResponse = await fetch(updateInfoUrl);
122
+
123
+ if (updateInfoResponse.ok) {
124
+ // todo: this seems brittle
125
+ updateInfo = await updateInfoResponse.json();
126
+
127
+ if (updateInfo.hash !== localInfo.hash) {
128
+ updateInfo.updateAvailable = true;
129
+ }
130
+ } else {
131
+ return {
132
+ version: "",
133
+ hash: "",
134
+ updateAvailable: false,
135
+ updateReady: false,
136
+ error: `Failed to fetch update info from ${updateInfoUrl}`,
137
+ };
138
+ }
139
+ } catch (error) {
140
+ return {
141
+ version: "",
142
+ hash: "",
143
+ updateAvailable: false,
144
+ updateReady: false,
145
+ error: `Failed to fetch update info from ${updateInfoUrl}`,
146
+ };
147
+ }
148
+
149
+ return updateInfo;
150
+ },
151
+
152
+ downloadUpdate: async () => {
153
+ const appDataFolder = await Updater.appDataFolder();
154
+ const channelBucketUrl = await Updater.channelBucketUrl();
155
+ const appFileName = localInfo.name;
156
+
157
+ let currentHash = (await Updater.getLocallocalInfo()).hash;
158
+ let latestHash = (await Updater.checkForUpdate()).hash;
159
+
160
+ const extractionFolder = join(appDataFolder, "self-extraction");
161
+ if (!(await Bun.file(extractionFolder).exists())) {
162
+ mkdirSync(extractionFolder, { recursive: true });
163
+ }
164
+
165
+ let currentTarPath = join(extractionFolder, `${currentHash}.tar`);
166
+ const latestTarPath = join(extractionFolder, `${latestHash}.tar`);
167
+
168
+ const seenHashes = [];
169
+
170
+ // todo (yoav): add a check to the while loop that checks for a hash we've seen before
171
+ // so that update loops that are cyclical can be broken
172
+ if (!(await Bun.file(latestTarPath).exists())) {
173
+ while (currentHash !== latestHash) {
174
+ seenHashes.push(currentHash);
175
+ const currentTar = Bun.file(currentTarPath);
176
+
177
+ if (!(await currentTar.exists())) {
178
+ // tar file of the current version not found
179
+ // so we can't patch it. We need the byte-for-byte tar file
180
+ // so break out and download the full version
181
+ break;
182
+ }
183
+
184
+ // check if there's a patch file for it
185
+ const platformFolder = `${localInfo.channel}-${currentOS}-${currentArch}`;
186
+ const patchResponse = await fetch(
187
+ join(localInfo.bucketUrl, platformFolder, `${currentHash}.patch`)
188
+ );
189
+
190
+ if (!patchResponse.ok) {
191
+ // patch not found
192
+ break;
193
+ }
194
+
195
+ // The patch file's name is the hash of the "from" version
196
+ const patchFilePath = join(
197
+ appDataFolder,
198
+ "self-extraction",
199
+ `${currentHash}.patch`
200
+ );
201
+ await Bun.write(patchFilePath, await patchResponse.arrayBuffer());
202
+ // patch it to a tmp name
203
+ const tmpPatchedTarFilePath = join(
204
+ appDataFolder,
205
+ "self-extraction",
206
+ `from-${currentHash}.tar`
207
+ );
208
+
209
+ // Note: cwd should be Contents/MacOS/ where the binaries are in the amc app bundle
210
+ try {
211
+ Bun.spawnSync([
212
+ "bspatch",
213
+ currentTarPath,
214
+ tmpPatchedTarFilePath,
215
+ patchFilePath,
216
+ ]);
217
+ } catch (error) {
218
+ break;
219
+ }
220
+
221
+ let versionSubpath = "";
222
+ const untarDir = join(appDataFolder, "self-extraction", "tmpuntar");
223
+ mkdirSync(untarDir, { recursive: true });
224
+
225
+ // extract just the version.json from the patched tar file so we can see what hash it is now
226
+ const resourcesDir = 'Resources'; // Always use capitalized Resources
227
+ await tar.x({
228
+ // gzip: false,
229
+ file: tmpPatchedTarFilePath,
230
+ cwd: untarDir,
231
+ filter: (path, stat) => {
232
+ if (path.endsWith(`${resourcesDir}/version.json`)) {
233
+ versionSubpath = path;
234
+ return true;
235
+ } else {
236
+ return false;
237
+ }
238
+ },
239
+ });
240
+
241
+ const currentVersionJson = await Bun.file(
242
+ join(untarDir, versionSubpath)
243
+ ).json();
244
+ const nextHash = currentVersionJson.hash;
245
+
246
+ if (seenHashes.includes(nextHash)) {
247
+ console.log("Warning: cyclical update detected");
248
+ break;
249
+ }
250
+
251
+ seenHashes.push(nextHash);
252
+
253
+ if (!nextHash) {
254
+ break;
255
+ }
256
+ // Sync the patched tar file to the new hash
257
+ const updatedTarPath = join(
258
+ appDataFolder,
259
+ "self-extraction",
260
+ `${nextHash}.tar`
261
+ );
262
+ renameSync(tmpPatchedTarFilePath, updatedTarPath);
263
+
264
+ // delete the old tar file
265
+ unlinkSync(currentTarPath);
266
+ unlinkSync(patchFilePath);
267
+ rmdirSync(untarDir, { recursive: true });
268
+
269
+ currentHash = nextHash;
270
+ currentTarPath = join(
271
+ appDataFolder,
272
+ "self-extraction",
273
+ `${currentHash}.tar`
274
+ );
275
+ // loop through applying patches until we reach the latest version
276
+ // if we get stuck then exit and just download the full latest version
277
+ }
278
+
279
+ // If we weren't able to apply patches to the current version,
280
+ // then just download it and unpack it
281
+ if (currentHash !== latestHash) {
282
+ const cacheBuster = Math.random().toString(36).substring(7);
283
+ const platformFolder = `${localInfo.channel}-${currentOS}-${currentArch}`;
284
+ // Platform-specific tarball naming
285
+ let tarballName: string;
286
+ if (currentOS === 'macos') {
287
+ tarballName = `${appFileName}.app.tar.zst`;
288
+ } else if (currentOS === 'win') {
289
+ tarballName = `${appFileName}.tar.zst`;
290
+ } else {
291
+ tarballName = `${appFileName}.tar.zst`;
292
+ }
293
+
294
+ const urlToLatestTarball = join(
295
+ localInfo.bucketUrl,
296
+ platformFolder,
297
+ tarballName
298
+ );
299
+ const prevVersionCompressedTarballPath = join(
300
+ appDataFolder,
301
+ "self-extraction",
302
+ "latest.tar.zst"
303
+ );
304
+ const response = await fetch(urlToLatestTarball + `?${cacheBuster}`);
305
+
306
+ if (response.ok && response.body) {
307
+ const reader = response.body.getReader();
308
+
309
+ const writer = Bun.file(prevVersionCompressedTarballPath).writer();
310
+
311
+ while (true) {
312
+ const { done, value } = await reader.read();
313
+ if (done) break;
314
+ await writer.write(value);
315
+ }
316
+ await writer.flush();
317
+ writer.end();
318
+ } else {
319
+ console.log("latest version not found at: ", urlToLatestTarball);
320
+ }
321
+
322
+ await ZstdInit().then(async ({ ZstdSimple }) => {
323
+ const data = new Uint8Array(
324
+ await Bun.file(prevVersionCompressedTarballPath).arrayBuffer()
325
+ );
326
+ const uncompressedData = ZstdSimple.decompress(data);
327
+
328
+ await Bun.write(latestTarPath, uncompressedData);
329
+ });
330
+
331
+ unlinkSync(prevVersionCompressedTarballPath);
332
+ try {
333
+ unlinkSync(currentTarPath);
334
+ } catch (error) {
335
+ // Note: ignore the error. it may have already been deleted by the patching process
336
+ // if the patching process only got halfway
337
+ }
338
+ }
339
+ }
340
+
341
+ // Note: Bun.file().exists() caches the result, so we nee d an new instance of Bun.file() here
342
+ // to check again
343
+ if (await Bun.file(latestTarPath).exists()) {
344
+ // download patch for this version, apply it.
345
+ // check for patch from that tar and apply it, until it matches the latest version
346
+ // as a fallback it should just download and unpack the latest version
347
+ updateInfo.updateReady = true;
348
+ } else {
349
+ updateInfo.error = "Failed to download latest version";
350
+ }
351
+ },
352
+
353
+ // todo (yoav): this should emit an event so app can cleanup or block the restart
354
+ // todo (yoav): rename this to quitAndApplyUpdate or something
355
+ applyUpdate: async () => {
356
+ if (updateInfo?.updateReady) {
357
+ const appDataFolder = await Updater.appDataFolder();
358
+ const extractionFolder = join(appDataFolder, "self-extraction");
359
+ if (!(await Bun.file(extractionFolder).exists())) {
360
+ mkdirSync(extractionFolder, { recursive: true });
361
+ }
362
+
363
+ let latestHash = (await Updater.checkForUpdate()).hash;
364
+ const latestTarPath = join(extractionFolder, `${latestHash}.tar`);
365
+
366
+ let appBundleSubpath: string = "";
367
+
368
+ if (await Bun.file(latestTarPath).exists()) {
369
+ // Windows needs a temporary directory to avoid file locking issues
370
+ const extractionDir = currentOS === 'win'
371
+ ? join(extractionFolder, `temp-${latestHash}`)
372
+ : extractionFolder;
373
+
374
+ if (currentOS === 'win') {
375
+ mkdirSync(extractionDir, { recursive: true });
376
+ }
377
+
378
+ // Use Windows native tar.exe on Windows due to npm tar library issues (same as CLI)
379
+ if (currentOS === 'win') {
380
+ console.log(`Using Windows native tar.exe to extract ${latestTarPath} to ${extractionDir}...`);
381
+ try {
382
+ const relativeTarPath = relative(extractionDir, latestTarPath);
383
+ execSync(`tar -xf "${relativeTarPath}"`, {
384
+ stdio: 'inherit',
385
+ cwd: extractionDir
386
+ });
387
+ console.log('Windows tar.exe extraction completed successfully');
388
+
389
+ // For Windows/Linux, the app bundle is at root level
390
+ appBundleSubpath = "./";
391
+ } catch (error) {
392
+ console.error('Windows tar.exe extraction failed:', error);
393
+ throw error;
394
+ }
395
+ } else {
396
+ // Use npm tar library on macOS/Linux (keep original behavior)
397
+ await tar.x({
398
+ // gzip: false,
399
+ file: latestTarPath,
400
+ cwd: extractionDir,
401
+ onentry: (entry) => {
402
+ if (currentOS === 'macos') {
403
+ // find the first .app bundle in the tarball
404
+ // Some apps may have nested .app bundles
405
+ if (!appBundleSubpath && entry.path.endsWith(".app/")) {
406
+ appBundleSubpath = entry.path;
407
+ }
408
+ } else {
409
+ // For Linux, look for the main executable
410
+ if (!appBundleSubpath) {
411
+ appBundleSubpath = "./";
412
+ }
413
+ }
414
+ },
415
+ });
416
+ }
417
+
418
+ console.log(`Tar extraction completed. Found appBundleSubpath: ${appBundleSubpath}`);
419
+
420
+ if (!appBundleSubpath) {
421
+ console.error("Failed to find app in tarball");
422
+ return;
423
+ }
424
+
425
+ // Note: resolve here removes the extra trailing / that the tar file adds
426
+ const extractedAppPath = resolve(
427
+ join(extractionDir, appBundleSubpath)
428
+ );
429
+
430
+ // Platform-specific path handling
431
+ let newAppBundlePath: string;
432
+ if (currentOS === 'linux' || currentOS === 'win') {
433
+ // On Linux/Windows, the actual app is inside a subdirectory
434
+ // Use same sanitization as extractor: remove spaces and dots
435
+ // Note: localInfo.name already includes the channel (e.g., "test1-canary")
436
+ const appBundleName = localInfo.name.replace(/ /g, "").replace(/\./g, "-");
437
+ newAppBundlePath = join(extractionDir, appBundleName);
438
+
439
+ // Verify the extracted app exists
440
+ if (!statSync(newAppBundlePath, { throwIfNoEntry: false })) {
441
+ console.error(`Extracted app not found at: ${newAppBundlePath}`);
442
+ console.log("Contents of extraction directory:");
443
+ try {
444
+ const files = readdirSync(extractionDir);
445
+ for (const file of files) {
446
+ console.log(` - ${file}`);
447
+ }
448
+ } catch (e) {
449
+ console.log("Could not list directory contents:", e);
450
+ }
451
+ return;
452
+ }
453
+ } else {
454
+ // On macOS, use the extracted app path directly
455
+ newAppBundlePath = extractedAppPath;
456
+ }
457
+ // Platform-specific app path calculation
458
+ let runningAppBundlePath: string;
459
+ if (currentOS === 'macos') {
460
+ // On macOS, executable is at Contents/MacOS/binary inside .app bundle
461
+ runningAppBundlePath = resolve(
462
+ dirname(process.execPath),
463
+ "..",
464
+ ".."
465
+ );
466
+ } else {
467
+ // On Linux/Windows, calculate app path using app data directory structure
468
+ const appDataFolder = await Updater.appDataFolder();
469
+ if (currentOS === 'linux') {
470
+ runningAppBundlePath = join(appDataFolder, "app");
471
+ } else {
472
+ // On Windows, use versioned app folders
473
+ const currentHash = (await Updater.getLocallocalInfo()).hash;
474
+ runningAppBundlePath = join(appDataFolder, `app-${currentHash}`);
475
+ }
476
+ }
477
+ // Platform-specific backup handling
478
+ let backupPath: string;
479
+ if (currentOS === 'macos') {
480
+ // On macOS, backup in extraction folder with .app extension
481
+ backupPath = join(extractionFolder, "backup.app");
482
+ } else {
483
+ // On Linux/Windows, create a tar backup of the current app
484
+ backupPath = join(extractionFolder, "backup.tar");
485
+ }
486
+
487
+ try {
488
+ if (currentOS === 'macos') {
489
+ // On macOS, use rename approach
490
+ // Remove existing backup if it exists
491
+ if (statSync(backupPath, { throwIfNoEntry: false })) {
492
+ rmdirSync(backupPath, { recursive: true });
493
+ }
494
+
495
+ // Move current running app to backup
496
+ renameSync(runningAppBundlePath, backupPath);
497
+
498
+ // Move new app to running location
499
+ renameSync(newAppBundlePath, runningAppBundlePath);
500
+ } else if (currentOS === 'linux') {
501
+ // On Linux, create tar backup and replace
502
+ // Remove existing backup.tar if it exists
503
+ if (statSync(backupPath, { throwIfNoEntry: false })) {
504
+ unlinkSync(backupPath);
505
+ }
506
+
507
+ // Create tar backup of current app
508
+ await tar.c(
509
+ {
510
+ file: backupPath,
511
+ cwd: dirname(runningAppBundlePath),
512
+ },
513
+ [basename(runningAppBundlePath)]
514
+ );
515
+
516
+ // Remove current app
517
+ rmdirSync(runningAppBundlePath, { recursive: true });
518
+
519
+ // Move new app to app location
520
+ renameSync(newAppBundlePath, runningAppBundlePath);
521
+
522
+ // Recreate run.sh launcher script
523
+ await createLinuxLauncherScript(runningAppBundlePath);
524
+ } else {
525
+ // On Windows, use versioned app folders
526
+ const parentDir = dirname(runningAppBundlePath);
527
+ const newVersionDir = join(parentDir, `app-${latestHash}`);
528
+
529
+ // Create the versioned directory
530
+ mkdirSync(newVersionDir, { recursive: true });
531
+
532
+ // Copy all contents from the extracted app to the versioned directory
533
+ const files = readdirSync(newAppBundlePath);
534
+ for (const file of files) {
535
+ const srcPath = join(newAppBundlePath, file);
536
+ const destPath = join(newVersionDir, file);
537
+ const stats = statSync(srcPath);
538
+
539
+ if (stats.isDirectory()) {
540
+ // Recursively copy directories
541
+ cpSync(srcPath, destPath, { recursive: true });
542
+ } else {
543
+ // Copy files
544
+ cpSync(srcPath, destPath);
545
+ }
546
+ }
547
+
548
+ // Clean up the temporary extraction directory on Windows
549
+ if (currentOS === 'win') {
550
+ rmdirSync(extractionDir, { recursive: true });
551
+ }
552
+
553
+ // Create/update the launcher batch file
554
+ const launcherPath = join(parentDir, "run.bat");
555
+ const launcherContent = `@echo off
556
+ :: Electrobun App Launcher
557
+ :: This file launches the current version
558
+
559
+ :: Set current version
560
+ set CURRENT_HASH=${latestHash}
561
+ set APP_DIR=%~dp0app-%CURRENT_HASH%
562
+
563
+ :: TODO: Implement proper cleanup mechanism that checks for running processes
564
+ :: For now, old versions are kept to avoid race conditions during updates
565
+ :: :: Clean up old app versions (keep current and one backup)
566
+ :: for /d %%D in ("%~dp0app-*") do (
567
+ :: if not "%%~nxD"=="app-%CURRENT_HASH%" (
568
+ :: echo Removing old version: %%~nxD
569
+ :: rmdir /s /q "%%D" 2>nul
570
+ :: )
571
+ :: )
572
+
573
+ :: Launch the app
574
+ cd /d "%APP_DIR%\\bin"
575
+ start "" launcher.exe
576
+ `;
577
+
578
+ await Bun.write(launcherPath, launcherContent);
579
+
580
+ // Update desktop shortcuts to point to run.bat
581
+ // This is handled by the running app, not the updater
582
+
583
+ runningAppBundlePath = newVersionDir;
584
+ }
585
+ } catch (error) {
586
+ console.error("Failed to replace app with new version", error);
587
+ return;
588
+ }
589
+
590
+ // Cross-platform app launch
591
+ switch (currentOS) {
592
+ case 'macos':
593
+ await Bun.spawn(["open", runningAppBundlePath]);
594
+ break;
595
+ case 'win':
596
+ // On Windows, launch the run.bat file which handles versioning
597
+ const parentDir = dirname(runningAppBundlePath);
598
+ const runBatPath = join(parentDir, "run.bat");
599
+
600
+
601
+ await Bun.spawn(["cmd", "/c", runBatPath], { detached: true });
602
+ break;
603
+ case 'linux':
604
+ // On Linux, use shell backgrounding to detach the process
605
+ const linuxLauncher = join(runningAppBundlePath, "bin", "launcher");
606
+ Bun.spawn(["sh", "-c", `${linuxLauncher} &`], { detached: true});
607
+ break;
608
+ }
609
+ // Use native killApp to properly clean up all resources on Windows/Linux
610
+ // macOS handles process.exit correctly
611
+ if (currentOS === 'linux' || currentOS === 'win') {
612
+ try {
613
+ native.symbols.killApp();
614
+ // Still call process.exit as a fallback
615
+ process.exit(0);
616
+ } catch (e) {
617
+ // Fallback if native binding fails
618
+ console.error('Failed to call native killApp:', e);
619
+ process.exit(0);
620
+ }
621
+ } else {
622
+ // macOS handles cleanup properly with process.exit
623
+ process.exit(0);
624
+ }
625
+ }
626
+ }
627
+ },
628
+
629
+ channelBucketUrl: async () => {
630
+ await Updater.getLocallocalInfo();
631
+ const platformFolder = `${localInfo.channel}-${currentOS}-${currentArch}`;
632
+ return join(localInfo.bucketUrl, platformFolder);
633
+ },
634
+
635
+ appDataFolder: async () => {
636
+ await Updater.getLocallocalInfo();
637
+ const appDataFolder = join(
638
+ getAppDataDir(),
639
+ localInfo.identifier,
640
+ localInfo.name
641
+ );
642
+
643
+ return appDataFolder;
644
+ },
645
+
646
+ // TODO: consider moving this from "Updater.localInfo" to "BuildVars"
647
+ localInfo: {
648
+ version: async () => {
649
+ return (await Updater.getLocallocalInfo()).version;
650
+ },
651
+ hash: async () => {
652
+ return (await Updater.getLocallocalInfo()).hash;
653
+ },
654
+ channel: async () => {
655
+ return (await Updater.getLocallocalInfo()).channel;
656
+ },
657
+ bucketUrl: async () => {
658
+ return (await Updater.getLocallocalInfo()).bucketUrl;
659
+ },
660
+ },
661
+
662
+ getLocallocalInfo: async () => {
663
+ if (localInfo) {
664
+ return localInfo;
665
+ }
666
+
667
+ try {
668
+ const resourcesDir = 'Resources'; // Always use capitalized Resources
669
+ localInfo = await Bun.file(`../${resourcesDir}/version.json`).json();
670
+ return localInfo;
671
+ } catch (error) {
672
+ // Handle the error
673
+ console.error("Failed to read version.json", error);
674
+
675
+ // Then rethrow so the app crashes
676
+ throw error;
677
+ }
678
+ },
679
+ };
680
+
681
+ export { Updater };