electrobun 0.0.19-beta.97 → 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 (77) hide show
  1. package/README.md +1 -1
  2. package/dist/api/browser/webviewtag.ts +54 -2
  3. package/dist/api/bun/ElectrobunConfig.ts +171 -0
  4. package/dist/api/bun/core/BrowserWindow.ts +4 -0
  5. package/dist/api/bun/core/Tray.ts +14 -0
  6. package/dist/api/bun/core/Updater.ts +4 -3
  7. package/dist/api/bun/index.ts +2 -0
  8. package/dist/api/bun/proc/native.ts +107 -5
  9. package/dist/main.js +5 -4
  10. package/package.json +4 -2
  11. package/src/cli/index.ts +565 -148
  12. package/templates/hello-world/bun.lock +164 -2
  13. package/templates/hello-world/electrobun.config.ts +28 -0
  14. package/templates/hello-world/src/bun/index.ts +2 -2
  15. package/templates/hello-world/src/mainview/index.html +5 -6
  16. package/templates/hello-world/src/mainview/index.ts +1 -5
  17. package/templates/interactive-playground/README.md +26 -0
  18. package/templates/interactive-playground/assets/tray-icon.png +0 -0
  19. package/templates/interactive-playground/electrobun.config.ts +36 -0
  20. package/templates/interactive-playground/package-lock.json +36 -0
  21. package/templates/interactive-playground/package.json +15 -0
  22. package/templates/interactive-playground/src/bun/demos/files.ts +70 -0
  23. package/templates/interactive-playground/src/bun/demos/menus.ts +139 -0
  24. package/templates/interactive-playground/src/bun/demos/rpc.ts +83 -0
  25. package/templates/interactive-playground/src/bun/demos/system.ts +72 -0
  26. package/templates/interactive-playground/src/bun/demos/updates.ts +105 -0
  27. package/templates/interactive-playground/src/bun/demos/windows.ts +90 -0
  28. package/templates/interactive-playground/src/bun/index.ts +124 -0
  29. package/templates/interactive-playground/src/bun/types/rpc.ts +109 -0
  30. package/templates/interactive-playground/src/mainview/components/EventLog.ts +107 -0
  31. package/templates/interactive-playground/src/mainview/components/Sidebar.ts +65 -0
  32. package/templates/interactive-playground/src/mainview/components/Toast.ts +57 -0
  33. package/templates/interactive-playground/src/mainview/demos/FileDemo.ts +211 -0
  34. package/templates/interactive-playground/src/mainview/demos/MenuDemo.ts +102 -0
  35. package/templates/interactive-playground/src/mainview/demos/RPCDemo.ts +229 -0
  36. package/templates/interactive-playground/src/mainview/demos/TrayDemo.ts +132 -0
  37. package/templates/interactive-playground/src/mainview/demos/WebViewDemo.ts +411 -0
  38. package/templates/interactive-playground/src/mainview/demos/WindowDemo.ts +207 -0
  39. package/templates/interactive-playground/src/mainview/index.css +538 -0
  40. package/templates/interactive-playground/src/mainview/index.html +103 -0
  41. package/templates/interactive-playground/src/mainview/index.ts +238 -0
  42. package/templates/multitab-browser/README.md +34 -0
  43. package/templates/multitab-browser/bun.lock +224 -0
  44. package/templates/multitab-browser/electrobun.config.ts +32 -0
  45. package/templates/multitab-browser/package-lock.json +20 -0
  46. package/templates/multitab-browser/package.json +12 -0
  47. package/templates/multitab-browser/src/bun/index.ts +144 -0
  48. package/templates/multitab-browser/src/bun/tabManager.ts +200 -0
  49. package/templates/multitab-browser/src/bun/types/rpc.ts +78 -0
  50. package/templates/multitab-browser/src/mainview/index.css +487 -0
  51. package/templates/multitab-browser/src/mainview/index.html +94 -0
  52. package/templates/multitab-browser/src/mainview/index.ts +634 -0
  53. package/templates/photo-booth/README.md +108 -0
  54. package/templates/photo-booth/bun.lock +239 -0
  55. package/templates/photo-booth/electrobun.config.ts +28 -0
  56. package/templates/photo-booth/package.json +16 -0
  57. package/templates/photo-booth/src/bun/index.ts +92 -0
  58. package/templates/photo-booth/src/mainview/index.css +465 -0
  59. package/templates/photo-booth/src/mainview/index.html +124 -0
  60. package/templates/photo-booth/src/mainview/index.ts +499 -0
  61. package/tests/bun.lock +14 -0
  62. package/tests/electrobun.config.ts +45 -0
  63. package/tests/package-lock.json +36 -0
  64. package/tests/package.json +13 -0
  65. package/tests/src/bun/index.ts +100 -0
  66. package/tests/src/bun/test-runner.ts +508 -0
  67. package/tests/src/mainview/index.html +110 -0
  68. package/tests/src/mainview/index.ts +458 -0
  69. package/tests/src/mainview/styles/main.css +451 -0
  70. package/tests/src/testviews/tray-test.html +57 -0
  71. package/tests/src/testviews/webview-mask.html +114 -0
  72. package/tests/src/testviews/webview-navigation.html +36 -0
  73. package/tests/src/testviews/window-create.html +17 -0
  74. package/tests/src/testviews/window-events.html +29 -0
  75. package/tests/src/testviews/window-focus.html +37 -0
  76. package/tests/src/webviewtag/index.ts +11 -0
  77. package/templates/hello-world/electrobun.config +0 -18
package/src/cli/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { join, dirname, basename } from "path";
1
+ import { join, dirname, basename, relative } from "path";
2
2
  import {
3
3
  existsSync,
4
4
  readFileSync,
@@ -15,6 +15,7 @@ import {
15
15
  copyFileSync,
16
16
  } from "fs";
17
17
  import { execSync } from "child_process";
18
+ import * as readline from "readline";
18
19
  import tar from "tar";
19
20
  import archiver from "archiver";
20
21
  import { ZstdInit } from "@oneidentity/zstd-js/wasm";
@@ -29,8 +30,12 @@ const MAX_CHUNK_SIZE = 1024 * 2;
29
30
 
30
31
  // this when run as an npm script this will be where the folder where package.json is.
31
32
  const projectRoot = process.cwd();
32
- const configName = "electrobun.config";
33
- const configPath = join(projectRoot, configName);
33
+
34
+ // Find TypeScript ESM config file
35
+ function findConfigFile(): string | null {
36
+ const configFile = join(projectRoot, 'electrobun.config.ts');
37
+ return existsSync(configFile) ? configFile : null;
38
+ }
34
39
 
35
40
  // Note: cli args can be called via npm bun /path/to/electorbun/binary arg1 arg2
36
41
  const indexOfElectrobun = process.argv.findIndex((arg) =>
@@ -191,7 +196,8 @@ async function ensureCoreDependencies(targetOS?: 'macos' | 'win' | 'linux', targ
191
196
  // Use Windows native tar.exe on Windows due to npm tar library issues
192
197
  if (OS === 'win') {
193
198
  console.log('Using Windows native tar.exe for reliable extraction...');
194
- execSync(`tar -xf "${tempFile}" -C "${platformDistPath}"`, {
199
+ const relativeTempFile = relative(platformDistPath, tempFile);
200
+ execSync(`tar -xf "${relativeTempFile}"`, {
195
201
  stdio: 'inherit',
196
202
  cwd: platformDistPath
197
203
  });
@@ -244,6 +250,9 @@ async function ensureCoreDependencies(targetOS?: 'macos' | 'win' | 'linux', targ
244
250
  console.error('This suggests the tarball structure is different than expected');
245
251
  }
246
252
 
253
+ // Note: We no longer need to remove or re-add signatures from downloaded binaries
254
+ // The CI-added adhoc signatures are actually required for macOS to run the binaries
255
+
247
256
  // For development: if main.js doesn't exist in shared dist/, copy from platform-specific download as fallback
248
257
  const sharedDistPath = join(ELECTROBUN_DEP_PATH, 'dist');
249
258
  const extractedMainJs = join(platformDistPath, 'main.js');
@@ -297,53 +306,164 @@ async function ensureCEFDependencies(targetOS?: 'macos' | 'win' | 'linux', targe
297
306
  const archName = platformArch;
298
307
  const cefTarballUrl = `https://github.com/blackboardsh/electrobun/releases/download/${version}/electrobun-cef-${platformName}-${archName}.tar.gz`;
299
308
 
300
- console.log(`Downloading CEF from: ${cefTarballUrl}`);
309
+ // Helper function to download with retry logic
310
+ async function downloadWithRetry(url: string, filePath: string, maxRetries = 3): Promise<void> {
311
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
312
+ try {
313
+ console.log(`Downloading CEF (attempt ${attempt}/${maxRetries}) from: ${url}`);
314
+
315
+ const response = await fetch(url);
316
+ if (!response.ok) {
317
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
318
+ }
319
+
320
+ // Get content length for progress tracking
321
+ const contentLength = response.headers.get('content-length');
322
+ const totalSize = contentLength ? parseInt(contentLength, 10) : 0;
323
+
324
+ // Create temp file with unique name to avoid conflicts
325
+ const fileStream = createWriteStream(filePath);
326
+ let downloadedSize = 0;
327
+ let lastReportedPercent = -1;
328
+
329
+ // Stream download with progress
330
+ if (response.body) {
331
+ const reader = response.body.getReader();
332
+ while (true) {
333
+ const { done, value } = await reader.read();
334
+ if (done) break;
335
+
336
+ const chunk = Buffer.from(value);
337
+ fileStream.write(chunk);
338
+ downloadedSize += chunk.length;
339
+
340
+ if (totalSize > 0) {
341
+ const percent = Math.round((downloadedSize / totalSize) * 100);
342
+ const percentTier = Math.floor(percent / 10) * 10;
343
+ if (percentTier > lastReportedPercent && percentTier <= 100) {
344
+ console.log(` Progress: ${percentTier}% (${Math.round(downloadedSize / 1024 / 1024)}MB/${Math.round(totalSize / 1024 / 1024)}MB)`);
345
+ lastReportedPercent = percentTier;
346
+ }
347
+ }
348
+ }
349
+ }
350
+
351
+ await new Promise((resolve, reject) => {
352
+ fileStream.end((error: any) => {
353
+ if (error) reject(error);
354
+ else resolve(void 0);
355
+ });
356
+ });
357
+
358
+ // Verify file size if content-length was provided
359
+ if (totalSize > 0) {
360
+ const actualSize = (await import('fs')).statSync(filePath).size;
361
+ if (actualSize !== totalSize) {
362
+ throw new Error(`Downloaded file size mismatch: expected ${totalSize}, got ${actualSize}`);
363
+ }
364
+ }
365
+
366
+ console.log(`✓ Download completed successfully (${Math.round(downloadedSize / 1024 / 1024)}MB)`);
367
+ return; // Success, exit retry loop
368
+
369
+ } catch (error: any) {
370
+ console.error(`Download attempt ${attempt} failed:`, error.message);
371
+
372
+ // Clean up partial download
373
+ if (existsSync(filePath)) {
374
+ unlinkSync(filePath);
375
+ }
376
+
377
+ if (attempt === maxRetries) {
378
+ throw new Error(`Failed to download after ${maxRetries} attempts: ${error.message}`);
379
+ }
380
+
381
+ // Wait before retrying (exponential backoff)
382
+ const delay = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s...
383
+ console.log(`Retrying in ${delay / 1000} seconds...`);
384
+ await new Promise(resolve => setTimeout(resolve, delay));
385
+ }
386
+ }
387
+ }
301
388
 
302
389
  try {
303
- // Download CEF tarball
304
- const response = await fetch(cefTarballUrl);
305
- if (!response.ok) {
306
- throw new Error(`Failed to download CEF: ${response.status} ${response.statusText}`);
307
- }
390
+ // Create temp file with unique name
391
+ const tempFile = join(ELECTROBUN_DEP_PATH, `cef-${platformOS}-${platformArch}-${Date.now()}.tar.gz`);
308
392
 
309
- // Create temp file
310
- const tempFile = join(ELECTROBUN_DEP_PATH, `cef-${platformOS}-${platformArch}-temp.tar.gz`);
311
- const fileStream = createWriteStream(tempFile);
312
-
313
- // Write response to file
314
- if (response.body) {
315
- const reader = response.body.getReader();
316
- while (true) {
317
- const { done, value } = await reader.read();
318
- if (done) break;
319
- fileStream.write(Buffer.from(value));
320
- }
321
- }
322
- fileStream.end();
393
+ // Download with retry logic
394
+ await downloadWithRetry(cefTarballUrl, tempFile);
323
395
 
324
396
  // Extract to platform-specific dist directory
325
397
  console.log(`Extracting CEF dependencies for ${platformOS}-${platformArch}...`);
326
398
  const platformDistPath = join(ELECTROBUN_DEP_PATH, `dist-${platformOS}-${platformArch}`);
327
399
  mkdirSync(platformDistPath, { recursive: true });
328
400
 
329
- // Use Windows native tar.exe on Windows due to npm tar library issues
330
- if (OS === 'win') {
331
- console.log('Using Windows native tar.exe for reliable extraction...');
332
- execSync(`tar -xf "${tempFile}" -C "${platformDistPath}"`, {
333
- stdio: 'inherit',
334
- cwd: platformDistPath
335
- });
336
- } else {
337
- await tar.x({
338
- file: tempFile,
339
- cwd: platformDistPath,
340
- preservePaths: false,
341
- strip: 0,
342
- });
401
+ // Helper function to validate tar file before extraction
402
+ async function validateTarFile(filePath: string): Promise<void> {
403
+ try {
404
+ // Quick validation - try to read the tar file header
405
+ const fd = await import('fs').then(fs => fs.promises.readFile(filePath));
406
+
407
+ // Check if it's a gzip file (magic bytes: 1f 8b)
408
+ if (fd.length < 2 || fd[0] !== 0x1f || fd[1] !== 0x8b) {
409
+ throw new Error('Invalid gzip header - file may be corrupted');
410
+ }
411
+
412
+ console.log(`✓ Tar file validation passed (${Math.round(fd.length / 1024 / 1024)}MB)`);
413
+ } catch (error: any) {
414
+ throw new Error(`Tar file validation failed: ${error.message}`);
415
+ }
343
416
  }
344
417
 
345
- // Clean up temp file
346
- unlinkSync(tempFile);
418
+ // Validate downloaded file before extraction
419
+ await validateTarFile(tempFile);
420
+
421
+ try {
422
+ // Use Windows native tar.exe on Windows due to npm tar library issues
423
+ if (OS === 'win') {
424
+ console.log('Using Windows native tar.exe for reliable extraction...');
425
+ const relativeTempFile = relative(platformDistPath, tempFile);
426
+ execSync(`tar -xf "${relativeTempFile}"`, {
427
+ stdio: 'inherit',
428
+ cwd: platformDistPath
429
+ });
430
+ } else {
431
+ await tar.x({
432
+ file: tempFile,
433
+ cwd: platformDistPath,
434
+ preservePaths: false,
435
+ strip: 0,
436
+ });
437
+ }
438
+
439
+ console.log(`✓ Extraction completed successfully`);
440
+
441
+ } catch (error: any) {
442
+ // Check if CEF directory was created despite the error (partial extraction)
443
+ const cefDir = join(platformDistPath, 'cef');
444
+ if (existsSync(cefDir)) {
445
+ const cefFiles = readdirSync(cefDir);
446
+ if (cefFiles.length > 0) {
447
+ console.warn(`⚠️ Extraction warning: ${error.message}`);
448
+ console.warn(` However, CEF files were extracted (${cefFiles.length} files found).`);
449
+ console.warn(` Proceeding with partial extraction - this usually works fine.`);
450
+ // Don't throw - continue with what we have
451
+ } else {
452
+ // No files extracted, this is a real failure
453
+ throw new Error(`Extraction failed (no files extracted): ${error.message}`);
454
+ }
455
+ } else {
456
+ // No CEF directory created, this is a real failure
457
+ throw new Error(`Extraction failed (no CEF directory created): ${error.message}`);
458
+ }
459
+ }
460
+
461
+ // Clean up temp file only after successful extraction
462
+ try {
463
+ unlinkSync(tempFile);
464
+ } catch (cleanupError) {
465
+ console.warn('Could not clean up temp file:', cleanupError);
466
+ }
347
467
 
348
468
  // Debug: List what was actually extracted for CEF
349
469
  try {
@@ -360,11 +480,28 @@ async function ensureCEFDependencies(targetOS?: 'macos' | 'win' | 'linux', targe
360
480
  console.error('Could not list CEF extracted files:', e);
361
481
  }
362
482
 
363
- console.log(`CEF dependencies for ${platformOS}-${platformArch} downloaded and cached successfully`);
483
+ console.log(`✓ CEF dependencies for ${platformOS}-${platformArch} downloaded and cached successfully`);
364
484
 
365
485
  } catch (error: any) {
366
486
  console.error(`Failed to download CEF dependencies for ${platformOS}-${platformArch}:`, error.message);
367
- console.error('Please ensure you have an internet connection and the release exists.');
487
+
488
+ // Provide helpful guidance based on the error
489
+ if (error.message.includes('corrupted download') || error.message.includes('zlib') || error.message.includes('unexpected end')) {
490
+ console.error('\n💡 This appears to be a download corruption issue. Suggestions:');
491
+ console.error(' • Check your internet connection stability');
492
+ console.error(' • Try running the command again (it will retry automatically)');
493
+ console.error(' • Clear the cache if the issue persists:');
494
+ console.error(` rm -rf "${ELECTROBUN_DEP_PATH}"`);
495
+ } else if (error.message.includes('HTTP 404') || error.message.includes('Not Found')) {
496
+ console.error('\n💡 The CEF release was not found. This could mean:');
497
+ console.error(' • The version specified doesn\'t have CEF binaries available');
498
+ console.error(' • You\'re using a development/unreleased version');
499
+ console.error(' • Try using a stable version instead');
500
+ } else {
501
+ console.error('\nPlease ensure you have an internet connection and the release exists.');
502
+ console.error(`If the problem persists, try clearing the cache: rm -rf "${ELECTROBUN_DEP_PATH}"`);
503
+ }
504
+
368
505
  process.exit(1);
369
506
  }
370
507
  }
@@ -402,6 +539,9 @@ const defaultConfig = {
402
539
  entitlements: {
403
540
  // This entitlement is required for Electrobun apps with a hardened runtime (required for notarization) to run on macos
404
541
  "com.apple.security.cs.allow-jit": true,
542
+ // Required for bun runtime to work with dynamic code execution and JIT compilation when signed
543
+ "com.apple.security.cs.allow-unsigned-executable-memory": true,
544
+ "com.apple.security.cs.disable-library-validation": true,
405
545
  },
406
546
  icons: "icon.iconset",
407
547
  },
@@ -431,19 +571,21 @@ if (!command) {
431
571
  process.exit(1);
432
572
  }
433
573
 
434
- const config = getConfig();
574
+ // Main execution function
575
+ async function main() {
576
+ const config = await getConfig();
435
577
 
436
- const envArg =
437
- process.argv.find((arg) => arg.startsWith("--env="))?.split("=")[1] || "";
578
+ const envArg =
579
+ process.argv.find((arg) => arg.startsWith("--env="))?.split("=")[1] || "";
438
580
 
439
- const targetsArg =
440
- process.argv.find((arg) => arg.startsWith("--targets="))?.split("=")[1] || "";
581
+ const targetsArg =
582
+ process.argv.find((arg) => arg.startsWith("--targets="))?.split("=")[1] || "";
441
583
 
442
- const validEnvironments = ["dev", "canary", "stable"];
584
+ const validEnvironments = ["dev", "canary", "stable"];
443
585
 
444
- // todo (yoav): dev, canary, and stable;
445
- const buildEnvironment: "dev" | "canary" | "stable" =
446
- validEnvironments.includes(envArg || "dev") ? (envArg || "dev") : "dev";
586
+ // todo (yoav): dev, canary, and stable;
587
+ const buildEnvironment: "dev" | "canary" | "stable" =
588
+ validEnvironments.includes(envArg || "dev") ? (envArg || "dev") : "dev";
447
589
 
448
590
  // Determine build targets
449
591
  type BuildTarget = { os: 'macos' | 'win' | 'linux', arch: 'arm64' | 'x64' };
@@ -678,6 +820,13 @@ function escapePathForTerminal(filePath: string) {
678
820
 
679
821
  return escapedPath;
680
822
  }
823
+
824
+ function sanitizeVolumeNameForHdiutil(volumeName: string) {
825
+ // Remove or replace characters that cause issues with hdiutil volume mounting
826
+ // Parentheses and other special characters can cause "Operation not permitted" errors
827
+ return volumeName.replace(/[()]/g, '');
828
+ }
829
+
681
830
  // MyApp
682
831
 
683
832
  // const appName = config.app.name.replace(/\s/g, '-').toLowerCase();
@@ -696,57 +845,117 @@ const bundleFileName = targetOS === 'macos' ? `${appFileName}.app` : appFileName
696
845
  let proc = null;
697
846
 
698
847
  if (commandArg === "init") {
699
- const projectName = process.argv[indexOfElectrobun + 2] || "my-electrobun-app";
700
- const templateName = process.argv.find(arg => arg.startsWith("--template="))?.split("=")[1] || "hello-world";
701
-
702
- console.log(`🚀 Initializing Electrobun project: ${projectName}`);
703
-
704
- // Validate template name
705
- const availableTemplates = getTemplateNames();
706
- if (!availableTemplates.includes(templateName)) {
707
- console.error(`❌ Template "${templateName}" not found.`);
708
- console.log(`Available templates: ${availableTemplates.join(", ")}`);
709
- process.exit(1);
710
- }
711
-
712
- const template = getTemplate(templateName);
713
- if (!template) {
714
- console.error(`❌ Could not load template "${templateName}"`);
715
- process.exit(1);
716
- }
717
-
718
- // Create project directory
719
- const projectPath = join(process.cwd(), projectName);
720
- if (existsSync(projectPath)) {
721
- console.error(`❌ Directory "${projectName}" already exists.`);
722
- process.exit(1);
723
- }
724
-
725
- mkdirSync(projectPath, { recursive: true });
726
-
727
- // Extract template files
728
- let fileCount = 0;
729
- for (const [relativePath, content] of Object.entries(template.files)) {
730
- const fullPath = join(projectPath, relativePath);
731
- const dir = dirname(fullPath);
848
+ await (async () => {
849
+ const secondArg = process.argv[indexOfElectrobun + 2];
850
+ const availableTemplates = getTemplateNames();
732
851
 
733
- // Create directory if it doesn't exist
734
- mkdirSync(dir, { recursive: true });
852
+ let projectName: string;
853
+ let templateName: string;
735
854
 
736
- // Write file
737
- writeFileSync(fullPath, content, 'utf-8');
738
- fileCount++;
739
- }
740
-
741
- console.log(`✅ Created ${fileCount} files from "${templateName}" template`);
742
- console.log(`📁 Project created at: ${projectPath}`);
743
- console.log("");
744
- console.log("📦 Next steps:");
745
- console.log(` cd ${projectName}`);
746
- console.log(" bun install");
747
- console.log(" bunx electrobun dev");
748
- console.log("");
749
- console.log("🎉 Happy building with Electrobun!");
855
+ // Check if --template= flag is used
856
+ const templateFlag = process.argv.find(arg => arg.startsWith("--template="));
857
+ if (templateFlag) {
858
+ // Traditional usage: electrobun init my-project --template=photo-booth
859
+ projectName = secondArg || "my-electrobun-app";
860
+ templateName = templateFlag.split("=")[1];
861
+ } else if (secondArg && availableTemplates.includes(secondArg)) {
862
+ // New intuitive usage: electrobun init photo-booth
863
+ projectName = secondArg; // Use template name as project name
864
+ templateName = secondArg;
865
+ } else {
866
+ // Interactive menu when no template specified
867
+ console.log("🚀 Welcome to Electrobun!");
868
+ console.log("");
869
+ console.log("Available templates:");
870
+ availableTemplates.forEach((template, index) => {
871
+ console.log(` ${index + 1}. ${template}`);
872
+ });
873
+ console.log("");
874
+
875
+ // Simple CLI selection using readline
876
+ const rl = readline.createInterface({
877
+ input: process.stdin,
878
+ output: process.stdout
879
+ });
880
+
881
+ const choice = await new Promise<string>((resolve) => {
882
+ rl.question('Select a template (enter number): ', (answer) => {
883
+ rl.close();
884
+ resolve(answer.trim());
885
+ });
886
+ });
887
+
888
+ const templateIndex = parseInt(choice) - 1;
889
+ if (templateIndex < 0 || templateIndex >= availableTemplates.length) {
890
+ console.error(`❌ Invalid selection. Please enter a number between 1 and ${availableTemplates.length}.`);
891
+ process.exit(1);
892
+ }
893
+
894
+ templateName = availableTemplates[templateIndex];
895
+
896
+ // Ask for project name
897
+ const rl2 = readline.createInterface({
898
+ input: process.stdin,
899
+ output: process.stdout
900
+ });
901
+
902
+ projectName = await new Promise<string>((resolve) => {
903
+ rl2.question(`Enter project name (default: my-${templateName}-app): `, (answer) => {
904
+ rl2.close();
905
+ resolve(answer.trim() || `my-${templateName}-app`);
906
+ });
907
+ });
908
+ }
909
+
910
+ console.log(`🚀 Initializing Electrobun project: ${projectName}`);
911
+ console.log(`📋 Using template: ${templateName}`);
912
+
913
+ // Validate template name
914
+ if (!availableTemplates.includes(templateName)) {
915
+ console.error(`❌ Template "${templateName}" not found.`);
916
+ console.log(`Available templates: ${availableTemplates.join(", ")}`);
917
+ process.exit(1);
918
+ }
919
+
920
+ const template = getTemplate(templateName);
921
+ if (!template) {
922
+ console.error(`❌ Could not load template "${templateName}"`);
923
+ process.exit(1);
924
+ }
925
+
926
+ // Create project directory
927
+ const projectPath = join(process.cwd(), projectName);
928
+ if (existsSync(projectPath)) {
929
+ console.error(`❌ Directory "${projectName}" already exists.`);
930
+ process.exit(1);
931
+ }
932
+
933
+ mkdirSync(projectPath, { recursive: true });
934
+
935
+ // Extract template files
936
+ let fileCount = 0;
937
+ for (const [relativePath, content] of Object.entries(template.files)) {
938
+ const fullPath = join(projectPath, relativePath);
939
+ const dir = dirname(fullPath);
940
+
941
+ // Create directory if it doesn't exist
942
+ mkdirSync(dir, { recursive: true });
943
+
944
+ // Write file
945
+ writeFileSync(fullPath, content, 'utf-8');
946
+ fileCount++;
947
+ }
948
+
949
+ console.log(`✅ Created ${fileCount} files from "${templateName}" template`);
950
+ console.log(`📁 Project created at: ${projectPath}`);
951
+ console.log("");
952
+ console.log("📦 Next steps:");
953
+ console.log(` cd ${projectName}`);
954
+ console.log(" bun install");
955
+ console.log(" bun start");
956
+ console.log("");
957
+ console.log("🎉 Happy building with Electrobun!");
958
+ })();
750
959
  } else if (commandArg === "build") {
751
960
  // Ensure core binaries are available for the target platform before starting build
752
961
  await ensureCoreDependencies(currentTarget.os, currentTarget.arch);
@@ -847,23 +1056,21 @@ if (commandArg === "init") {
847
1056
  // mkdirSync(destLauncherFolder, {recursive: true});
848
1057
  // }
849
1058
  // cpSync(zigLauncherBinarySource, zigLauncherDestination, {recursive: true, dereference: true});
850
- // Only copy launcher for non-dev builds
851
- if (buildEnvironment !== "dev") {
852
- const bunCliLauncherBinarySource = targetPaths.LAUNCHER_RELEASE;
853
- const bunCliLauncherDestination = join(appBundleMacOSPath, "launcher") + targetBinExt;
854
- const destLauncherFolder = dirname(bunCliLauncherDestination);
855
- if (!existsSync(destLauncherFolder)) {
856
- // console.info('creating folder: ', destFolder);
857
- mkdirSync(destLauncherFolder, { recursive: true });
858
- }
859
-
860
- cpSync(bunCliLauncherBinarySource, bunCliLauncherDestination, {
861
- recursive: true,
862
- dereference: true,
863
- });
1059
+ // Copy zig launcher for all builds (dev, canary, stable)
1060
+ const bunCliLauncherBinarySource = targetPaths.LAUNCHER_RELEASE;
1061
+ const bunCliLauncherDestination = join(appBundleMacOSPath, "launcher") + targetBinExt;
1062
+ const destLauncherFolder = dirname(bunCliLauncherDestination);
1063
+ if (!existsSync(destLauncherFolder)) {
1064
+ // console.info('creating folder: ', destFolder);
1065
+ mkdirSync(destLauncherFolder, { recursive: true });
864
1066
  }
865
1067
 
866
- cpSync(targetPaths.MAIN_JS, join(appBundleMacOSPath, 'main.js'));
1068
+ cpSync(bunCliLauncherBinarySource, bunCliLauncherDestination, {
1069
+ recursive: true,
1070
+ dereference: true,
1071
+ });
1072
+
1073
+ cpSync(targetPaths.MAIN_JS, join(appBundleFolderResourcesPath, 'main.js'));
867
1074
 
868
1075
  // Bun runtime binary
869
1076
  // todo (yoav): this only works for the current architecture
@@ -1271,11 +1478,13 @@ if (commandArg === "init") {
1271
1478
 
1272
1479
  // Run postBuild script
1273
1480
  if (config.scripts.postBuild) {
1481
+ console.log("Running postBuild script:", config.scripts.postBuild);
1274
1482
  // Use host platform's bun binary for running scripts, not target platform's
1275
1483
  const hostPaths = getPlatformPaths(OS, ARCH);
1276
1484
 
1277
- Bun.spawnSync([hostPaths.BUN_BINARY, config.scripts.postBuild], {
1485
+ const result = Bun.spawnSync([hostPaths.BUN_BINARY, config.scripts.postBuild], {
1278
1486
  stdio: ["ignore", "inherit", "inherit"],
1487
+ cwd: projectRoot, // Add cwd to ensure script runs from project root
1279
1488
  env: {
1280
1489
  ...process.env,
1281
1490
  ELECTROBUN_BUILD_ENV: buildEnvironment,
@@ -1288,6 +1497,18 @@ if (commandArg === "init") {
1288
1497
  ELECTROBUN_ARTIFACT_DIR: artifactFolder,
1289
1498
  },
1290
1499
  });
1500
+
1501
+ if (result.exitCode !== 0) {
1502
+ console.error("postBuild script failed with exit code:", result.exitCode);
1503
+ if (result.stderr) {
1504
+ console.error("stderr:", result.stderr.toString());
1505
+ }
1506
+ // Also log which bun binary we're trying to use
1507
+ console.error("Tried to run with bun at:", hostPaths.BUN_BINARY);
1508
+ console.error("Script path:", config.scripts.postBuild);
1509
+ console.error("Working directory:", projectRoot);
1510
+ process.exit(1);
1511
+ }
1291
1512
  }
1292
1513
  // All the unique files are in the bundle now. Create an initial temporary tar file
1293
1514
  // for hashing the contents
@@ -1476,7 +1697,7 @@ if (commandArg === "init") {
1476
1697
  // hdiutil create -volname "YourAppName" -srcfolder /path/to/YourApp.app -ov -format UDZO YourAppName.dmg
1477
1698
  // Note: use ULFO (lzfse) for better compatibility with large CEF frameworks and modern macOS
1478
1699
  execSync(
1479
- `hdiutil create -volname "${appFileName}" -srcfolder ${escapePathForTerminal(
1700
+ `hdiutil create -volname "${sanitizeVolumeNameForHdiutil(appFileName)}" -srcfolder ${escapePathForTerminal(
1480
1701
  selfExtractingBundle.appBundleFolderPath
1481
1702
  )} -ov -format ULFO ${escapePathForTerminal(dmgPath)}`
1482
1703
  );
@@ -1741,24 +1962,27 @@ exec "\$LAUNCHER_BINARY" "\$@"
1741
1962
 
1742
1963
  let mainProc;
1743
1964
  let bundleExecPath: string;
1965
+ let bundleResourcesPath: string;
1744
1966
 
1745
1967
  if (OS === 'macos') {
1746
1968
  bundleExecPath = join(buildFolder, bundleFileName, "Contents", 'MacOS');
1969
+ bundleResourcesPath = join(buildFolder, bundleFileName, "Contents", 'Resources');
1747
1970
  } else if (OS === 'linux' || OS === 'win') {
1748
1971
  bundleExecPath = join(buildFolder, bundleFileName, "bin");
1972
+ bundleResourcesPath = join(buildFolder, bundleFileName, "Resources");
1749
1973
  } else {
1750
1974
  throw new Error(`Unsupported OS: ${OS}`);
1751
1975
  }
1752
1976
 
1753
1977
  if (OS === 'macos') {
1754
-
1755
- mainProc = Bun.spawn([join(bundleExecPath,'bun'), join(bundleExecPath, 'main.js')], {
1978
+ // Use the zig launcher for all builds (dev, canary, stable)
1979
+ mainProc = Bun.spawn([join(bundleExecPath, 'launcher')], {
1756
1980
  stdio: ['inherit', 'inherit', 'inherit'],
1757
1981
  cwd: bundleExecPath
1758
1982
  })
1759
1983
  } else if (OS === 'win') {
1760
- // Try the main process
1761
- mainProc = Bun.spawn(['./bun.exe', './main.js'], {
1984
+ // Try the main process - use relative path to Resources folder
1985
+ mainProc = Bun.spawn(['./bun.exe', '../Resources/main.js'], {
1762
1986
  stdio: ['inherit', 'inherit', 'inherit'],
1763
1987
  cwd: bundleExecPath,
1764
1988
  onExit: (proc, exitCode, signalCode, error) => {
@@ -1779,7 +2003,7 @@ exec "\$LAUNCHER_BINARY" "\$@"
1779
2003
  }
1780
2004
  }
1781
2005
 
1782
- mainProc = Bun.spawn([join(bundleExecPath, 'bun'), join(bundleExecPath, 'main.js')], {
2006
+ mainProc = Bun.spawn([join(bundleExecPath, 'bun'), join(bundleResourcesPath, 'main.js')], {
1783
2007
  stdio: ['inherit', 'inherit', 'inherit'],
1784
2008
  cwd: bundleExecPath,
1785
2009
  env
@@ -1787,23 +2011,49 @@ exec "\$LAUNCHER_BINARY" "\$@"
1787
2011
  }
1788
2012
 
1789
2013
  process.on("SIGINT", () => {
1790
- console.log('exit command')
1791
- // toLauncherPipe.write("exit command\n");
1792
- mainProc.kill();
1793
- process.exit();
2014
+ console.log('[electrobun dev] Received SIGINT, initiating graceful shutdown...')
2015
+
2016
+ if (mainProc) {
2017
+ // First attempt graceful shutdown by sending SIGINT to child
2018
+ console.log('[electrobun dev] Requesting graceful shutdown from app...')
2019
+ mainProc.kill("SIGINT");
2020
+
2021
+ // Give the app time to clean up (e.g., call killApp())
2022
+ setTimeout(() => {
2023
+ if (mainProc && !mainProc.killed) {
2024
+ console.log('[electrobun dev] App did not exit gracefully, forcing termination...')
2025
+ mainProc.kill("SIGKILL");
2026
+ }
2027
+ process.exit(0);
2028
+ }, 2000); // 2 second timeout for graceful shutdown
2029
+ } else {
2030
+ process.exit(0);
2031
+ }
1794
2032
  });
1795
2033
 
1796
2034
  }
1797
2035
 
1798
- function getConfig() {
2036
+ async function getConfig() {
1799
2037
  let loadedConfig = {};
1800
- if (existsSync(configPath)) {
1801
- const configFileContents = readFileSync(configPath, "utf8");
1802
- // Note: we want this to hard fail if there's a syntax error
2038
+ const foundConfigPath = findConfigFile();
2039
+
2040
+ if (foundConfigPath) {
2041
+ console.log(`Using config file: ${basename(foundConfigPath)}`);
2042
+
1803
2043
  try {
1804
- loadedConfig = JSON.parse(configFileContents);
2044
+ // Use dynamic import for TypeScript ESM files
2045
+ // Bun handles TypeScript natively, no transpilation needed
2046
+ const configModule = await import(foundConfigPath);
2047
+ loadedConfig = configModule.default || configModule;
2048
+
2049
+ // Validate that we got a valid config object
2050
+ if (!loadedConfig || typeof loadedConfig !== 'object') {
2051
+ console.error("Config file must export a default object");
2052
+ console.error("using default config instead");
2053
+ loadedConfig = {};
2054
+ }
1805
2055
  } catch (error) {
1806
- console.error("Failed to parse config file:", error);
2056
+ console.error("Failed to load config file:", error);
1807
2057
  console.error("using default config instead");
1808
2058
  }
1809
2059
  }
@@ -2187,30 +2437,189 @@ function codesignAppBundle(
2187
2437
  process.exit(1);
2188
2438
  }
2189
2439
 
2190
- // list of entitlements https://developer.apple.com/documentation/security/hardened_runtime?language=objc
2191
- // todo (yoav): consider allowing separate entitlements config for each binary
2192
- // const entitlementsFilePath = join(buildFolder, 'entitlements.plist');
2193
-
2194
- // codesign --deep --force --verbose --timestamp --sign "ELECTROBUN_DEVELOPER_ID" --options runtime --entitlements entitlementsFilePath appBundleOrDmgPath`
2440
+ // If this is a DMG file, sign it directly
2441
+ if (appBundleOrDmgPath.endsWith('.dmg')) {
2442
+ execSync(
2443
+ `codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" ${escapePathForTerminal(
2444
+ appBundleOrDmgPath
2445
+ )}`
2446
+ );
2447
+ return;
2448
+ }
2195
2449
 
2450
+ // For app bundles, sign binaries individually to avoid --deep issues with notarization
2451
+ const contentsPath = join(appBundleOrDmgPath, 'Contents');
2452
+ const macosPath = join(contentsPath, 'MacOS');
2453
+
2454
+ // Prepare entitlements if provided
2196
2455
  if (entitlementsFilePath) {
2197
2456
  const entitlementsFileContents = buildEntitlementsFile(
2198
2457
  config.build.mac.entitlements
2199
2458
  );
2200
2459
  Bun.write(entitlementsFilePath, entitlementsFileContents);
2460
+ }
2201
2461
 
2202
- execSync(
2203
- `codesign --deep --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" --options runtime --entitlements ${entitlementsFilePath} ${escapePathForTerminal(
2204
- appBundleOrDmgPath
2205
- )}`
2206
- );
2207
- } else {
2208
- execSync(
2209
- `codesign --deep --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" ${escapePathForTerminal(
2210
- appBundleOrDmgPath
2211
- )}`
2212
- );
2462
+ // Sign frameworks first (CEF framework requires special handling)
2463
+ const frameworksPath = join(contentsPath, 'Frameworks');
2464
+ if (existsSync(frameworksPath)) {
2465
+ try {
2466
+ const frameworks = readdirSync(frameworksPath);
2467
+ for (const framework of frameworks) {
2468
+ if (framework.endsWith('.framework')) {
2469
+ const frameworkPath = join(frameworksPath, framework);
2470
+
2471
+ if (framework === 'Chromium Embedded Framework.framework') {
2472
+ console.log(`Signing CEF framework components: ${framework}`);
2473
+
2474
+ // Sign CEF libraries first
2475
+ const librariesPath = join(frameworkPath, 'Libraries');
2476
+ if (existsSync(librariesPath)) {
2477
+ const libraries = readdirSync(librariesPath);
2478
+ for (const library of libraries) {
2479
+ if (library.endsWith('.dylib')) {
2480
+ const libraryPath = join(librariesPath, library);
2481
+ console.log(`Signing CEF library: ${library}`);
2482
+ execSync(
2483
+ `codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" --options runtime ${escapePathForTerminal(libraryPath)}`
2484
+ );
2485
+ }
2486
+ }
2487
+ }
2488
+
2489
+ // CEF helper apps are in the main Frameworks directory, not inside the CEF framework
2490
+ // We'll sign them after signing all frameworks
2491
+ }
2492
+
2493
+ // Sign the framework bundle itself (for CEF and any other frameworks)
2494
+ console.log(`Signing framework bundle: ${framework}`);
2495
+ execSync(
2496
+ `codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" --options runtime ${escapePathForTerminal(frameworkPath)}`
2497
+ );
2498
+ }
2499
+ }
2500
+ } catch (err) {
2501
+ console.log("Error signing frameworks:", err);
2502
+ throw err; // Re-throw to fail the build since framework signing is critical
2503
+ }
2504
+ }
2505
+
2506
+ // Sign CEF helper apps (they're in the main Frameworks directory, not inside CEF framework)
2507
+ const cefHelperApps = [
2508
+ 'bun Helper.app',
2509
+ 'bun Helper (GPU).app',
2510
+ 'bun Helper (Plugin).app',
2511
+ 'bun Helper (Alerts).app',
2512
+ 'bun Helper (Renderer).app'
2513
+ ];
2514
+
2515
+ for (const helperApp of cefHelperApps) {
2516
+ const helperPath = join(frameworksPath, helperApp);
2517
+ if (existsSync(helperPath)) {
2518
+ const helperExecutablePath = join(helperPath, 'Contents', 'MacOS', helperApp.replace('.app', ''));
2519
+ if (existsSync(helperExecutablePath)) {
2520
+ console.log(`Signing CEF helper executable: ${helperApp}`);
2521
+ const entitlementFlag = entitlementsFilePath ? `--entitlements ${entitlementsFilePath}` : '';
2522
+ execSync(
2523
+ `codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" --options runtime ${entitlementFlag} ${escapePathForTerminal(helperExecutablePath)}`
2524
+ );
2525
+ }
2526
+
2527
+ console.log(`Signing CEF helper bundle: ${helperApp}`);
2528
+ const entitlementFlag = entitlementsFilePath ? `--entitlements ${entitlementsFilePath}` : '';
2529
+ execSync(
2530
+ `codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" --options runtime ${entitlementFlag} ${escapePathForTerminal(helperPath)}`
2531
+ );
2532
+ }
2533
+ }
2534
+
2535
+ // Sign all binaries and libraries in MacOS folder and subdirectories
2536
+ console.log("Signing all binaries in MacOS folder...");
2537
+
2538
+ // Recursively find all executables and libraries in MacOS folder
2539
+ function findExecutables(dir: string): string[] {
2540
+ let executables: string[] = [];
2541
+
2542
+ try {
2543
+ const entries = readdirSync(dir, { withFileTypes: true });
2544
+
2545
+ for (const entry of entries) {
2546
+ const fullPath = join(dir, entry.name);
2547
+
2548
+ if (entry.isDirectory()) {
2549
+ // Recursively search subdirectories
2550
+ executables = executables.concat(findExecutables(fullPath));
2551
+ } else if (entry.isFile()) {
2552
+ // Check if it's an executable or library
2553
+ try {
2554
+ const fileInfo = execSync(`file -b "${fullPath}"`, { encoding: 'utf8' }).trim();
2555
+ if (fileInfo.includes('Mach-O') || entry.name.endsWith('.dylib')) {
2556
+ executables.push(fullPath);
2557
+ }
2558
+ } catch {
2559
+ // If file command fails, check by extension
2560
+ if (entry.name.endsWith('.dylib') || !entry.name.includes('.')) {
2561
+ // No extension often means executable
2562
+ executables.push(fullPath);
2563
+ }
2564
+ }
2565
+ }
2566
+ }
2567
+ } catch (err) {
2568
+ console.error(`Error scanning directory ${dir}:`, err);
2569
+ }
2570
+
2571
+ return executables;
2213
2572
  }
2573
+
2574
+ const executablesInMacOS = findExecutables(macosPath);
2575
+
2576
+ // Sign each found executable
2577
+ for (const execPath of executablesInMacOS) {
2578
+ const fileName = basename(execPath);
2579
+ const relativePath = execPath.replace(macosPath + '/', '');
2580
+
2581
+ // Use filename as identifier (without extension)
2582
+ const identifier = fileName.replace(/\.[^.]+$/, '');
2583
+
2584
+ console.log(`Signing ${relativePath} with identifier ${identifier}`);
2585
+ const entitlementFlag = entitlementsFilePath ? `--entitlements ${entitlementsFilePath}` : '';
2586
+
2587
+ try {
2588
+ execSync(
2589
+ `codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" --options runtime --identifier ${identifier} ${entitlementFlag} ${escapePathForTerminal(execPath)}`
2590
+ );
2591
+ } catch (err) {
2592
+ console.error(`Failed to sign ${relativePath}:`, err.message);
2593
+ // Continue signing other files even if one fails
2594
+ }
2595
+ }
2596
+
2597
+ // Note: main.js is now in Resources and will be automatically sealed when signing the app bundle
2598
+
2599
+ // Sign the main executable (launcher) - this should use the app's bundle identifier, not "launcher"
2600
+ const launcherPath = join(macosPath, 'launcher');
2601
+ if (existsSync(launcherPath)) {
2602
+ console.log("Signing main executable (launcher)");
2603
+ const entitlementFlag = entitlementsFilePath ? `--entitlements ${entitlementsFilePath}` : '';
2604
+ try {
2605
+ execSync(
2606
+ `codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" --options runtime ${entitlementFlag} ${escapePathForTerminal(launcherPath)}`
2607
+ );
2608
+ } catch (error) {
2609
+ console.error("Failed to sign launcher:", error.message);
2610
+ console.log("Attempting to sign launcher without runtime hardening...");
2611
+ execSync(
2612
+ `codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" ${entitlementFlag} ${escapePathForTerminal(launcherPath)}`
2613
+ );
2614
+ }
2615
+ }
2616
+
2617
+ // Finally, sign the app bundle itself (without --deep)
2618
+ console.log("Signing app bundle");
2619
+ const entitlementFlag = entitlementsFilePath ? `--entitlements ${entitlementsFilePath}` : '';
2620
+ execSync(
2621
+ `codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" --options runtime ${entitlementFlag} ${escapePathForTerminal(appBundleOrDmgPath)}`
2622
+ );
2214
2623
  }
2215
2624
 
2216
2625
  function notarizeAndStaple(appOrDmgPath: string) {
@@ -2349,3 +2758,11 @@ function createAppBundle(bundleName: string, parentFolder: string, targetOS: 'ma
2349
2758
  throw new Error(`Unsupported OS: ${targetOS}`);
2350
2759
  }
2351
2760
  }
2761
+
2762
+ } // End of main() function
2763
+
2764
+ // Run the main function
2765
+ main().catch((error) => {
2766
+ console.error('Fatal error:', error);
2767
+ process.exit(1);
2768
+ });