electrobun 0.0.19-beta.13 → 0.0.19-beta.131
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BUILD.md +90 -0
- package/README.md +1 -1
- package/bin/electrobun.cjs +2 -9
- package/debug.js +5 -0
- package/dist/api/browser/builtinrpcSchema.ts +19 -0
- package/dist/api/browser/index.ts +409 -0
- package/dist/api/browser/rpc/webview.ts +79 -0
- package/dist/api/browser/stylesAndElements.ts +3 -0
- package/dist/api/browser/webviewtag.ts +586 -0
- package/dist/api/bun/ElectrobunConfig.ts +171 -0
- package/dist/api/bun/core/ApplicationMenu.ts +66 -0
- package/dist/api/bun/core/BrowserView.ts +349 -0
- package/dist/api/bun/core/BrowserWindow.ts +195 -0
- package/dist/api/bun/core/ContextMenu.ts +67 -0
- package/dist/api/bun/core/Paths.ts +5 -0
- package/dist/api/bun/core/Socket.ts +181 -0
- package/dist/api/bun/core/Tray.ts +121 -0
- package/dist/api/bun/core/Updater.ts +681 -0
- package/dist/api/bun/core/Utils.ts +48 -0
- package/dist/api/bun/events/ApplicationEvents.ts +14 -0
- package/dist/api/bun/events/event.ts +29 -0
- package/dist/api/bun/events/eventEmitter.ts +45 -0
- package/dist/api/bun/events/trayEvents.ts +9 -0
- package/dist/api/bun/events/webviewEvents.ts +16 -0
- package/dist/api/bun/events/windowEvents.ts +12 -0
- package/dist/api/bun/index.ts +47 -0
- package/dist/api/bun/proc/linux.md +43 -0
- package/dist/api/bun/proc/native.ts +1322 -0
- package/dist/api/shared/platform.ts +48 -0
- package/dist/main.js +54 -0
- package/package.json +11 -6
- package/src/cli/index.ts +1353 -239
- package/templates/hello-world/README.md +57 -0
- package/templates/hello-world/bun.lock +225 -0
- package/templates/hello-world/electrobun.config.ts +28 -0
- package/templates/hello-world/package.json +16 -0
- package/templates/hello-world/src/bun/index.ts +15 -0
- package/templates/hello-world/src/mainview/index.css +124 -0
- package/templates/hello-world/src/mainview/index.html +46 -0
- package/templates/hello-world/src/mainview/index.ts +1 -0
- package/templates/interactive-playground/README.md +26 -0
- package/templates/interactive-playground/assets/tray-icon.png +0 -0
- package/templates/interactive-playground/electrobun.config.ts +36 -0
- package/templates/interactive-playground/package-lock.json +36 -0
- package/templates/interactive-playground/package.json +15 -0
- package/templates/interactive-playground/src/bun/demos/files.ts +70 -0
- package/templates/interactive-playground/src/bun/demos/menus.ts +139 -0
- package/templates/interactive-playground/src/bun/demos/rpc.ts +83 -0
- package/templates/interactive-playground/src/bun/demos/system.ts +72 -0
- package/templates/interactive-playground/src/bun/demos/updates.ts +105 -0
- package/templates/interactive-playground/src/bun/demos/windows.ts +90 -0
- package/templates/interactive-playground/src/bun/index.ts +124 -0
- package/templates/interactive-playground/src/bun/types/rpc.ts +109 -0
- package/templates/interactive-playground/src/mainview/components/EventLog.ts +107 -0
- package/templates/interactive-playground/src/mainview/components/Sidebar.ts +65 -0
- package/templates/interactive-playground/src/mainview/components/Toast.ts +57 -0
- package/templates/interactive-playground/src/mainview/demos/FileDemo.ts +211 -0
- package/templates/interactive-playground/src/mainview/demos/MenuDemo.ts +102 -0
- package/templates/interactive-playground/src/mainview/demos/RPCDemo.ts +229 -0
- package/templates/interactive-playground/src/mainview/demos/TrayDemo.ts +132 -0
- package/templates/interactive-playground/src/mainview/demos/WebViewDemo.ts +411 -0
- package/templates/interactive-playground/src/mainview/demos/WindowDemo.ts +207 -0
- package/templates/interactive-playground/src/mainview/index.css +538 -0
- package/templates/interactive-playground/src/mainview/index.html +103 -0
- package/templates/interactive-playground/src/mainview/index.ts +238 -0
- package/templates/multitab-browser/README.md +34 -0
- package/templates/multitab-browser/bun.lock +224 -0
- package/templates/multitab-browser/electrobun.config.ts +32 -0
- package/templates/multitab-browser/package-lock.json +20 -0
- package/templates/multitab-browser/package.json +12 -0
- package/templates/multitab-browser/src/bun/index.ts +144 -0
- package/templates/multitab-browser/src/bun/tabManager.ts +200 -0
- package/templates/multitab-browser/src/bun/types/rpc.ts +78 -0
- package/templates/multitab-browser/src/mainview/index.css +487 -0
- package/templates/multitab-browser/src/mainview/index.html +94 -0
- package/templates/multitab-browser/src/mainview/index.ts +634 -0
- package/templates/photo-booth/README.md +108 -0
- package/templates/photo-booth/bun.lock +239 -0
- package/templates/photo-booth/electrobun.config.ts +28 -0
- package/templates/photo-booth/package.json +16 -0
- package/templates/photo-booth/src/bun/index.ts +92 -0
- package/templates/photo-booth/src/mainview/index.css +465 -0
- package/templates/photo-booth/src/mainview/index.html +124 -0
- package/templates/photo-booth/src/mainview/index.ts +499 -0
- package/tests/bun.lock +14 -0
- package/tests/electrobun.config.ts +45 -0
- package/tests/package-lock.json +36 -0
- package/tests/package.json +13 -0
- package/tests/src/bun/index.ts +100 -0
- package/tests/src/bun/test-runner.ts +508 -0
- package/tests/src/mainview/index.html +110 -0
- package/tests/src/mainview/index.ts +458 -0
- package/tests/src/mainview/styles/main.css +451 -0
- package/tests/src/testviews/tray-test.html +57 -0
- package/tests/src/testviews/webview-mask.html +114 -0
- package/tests/src/testviews/webview-navigation.html +36 -0
- package/tests/src/testviews/window-create.html +17 -0
- package/tests/src/testviews/window-events.html +29 -0
- package/tests/src/testviews/window-focus.html +37 -0
- 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 };
|