electrobun 0.0.19-beta.99 → 0.1.1

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 +621 -151
  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 +32 -0
  56. package/templates/photo-booth/package.json +17 -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
  },
@@ -424,6 +564,55 @@ const defaultConfig = {
424
564
  },
425
565
  };
426
566
 
567
+ // Mapping of entitlements to their corresponding Info.plist usage description keys
568
+ const ENTITLEMENT_TO_PLIST_KEY: Record<string, string> = {
569
+ "com.apple.security.device.camera": "NSCameraUsageDescription",
570
+ "com.apple.security.device.microphone": "NSMicrophoneUsageDescription",
571
+ "com.apple.security.device.audio-input": "NSMicrophoneUsageDescription",
572
+ "com.apple.security.personal-information.location": "NSLocationUsageDescription",
573
+ "com.apple.security.personal-information.location-when-in-use": "NSLocationWhenInUseUsageDescription",
574
+ "com.apple.security.personal-information.contacts": "NSContactsUsageDescription",
575
+ "com.apple.security.personal-information.calendars": "NSCalendarsUsageDescription",
576
+ "com.apple.security.personal-information.reminders": "NSRemindersUsageDescription",
577
+ "com.apple.security.personal-information.photos-library": "NSPhotoLibraryUsageDescription",
578
+ "com.apple.security.personal-information.apple-music-library": "NSAppleMusicUsageDescription",
579
+ "com.apple.security.personal-information.motion": "NSMotionUsageDescription",
580
+ "com.apple.security.personal-information.speech-recognition": "NSSpeechRecognitionUsageDescription",
581
+ "com.apple.security.device.bluetooth": "NSBluetoothAlwaysUsageDescription",
582
+ "com.apple.security.files.user-selected.read-write": "NSDocumentsFolderUsageDescription",
583
+ "com.apple.security.files.downloads.read-write": "NSDownloadsFolderUsageDescription",
584
+ "com.apple.security.files.desktop.read-write": "NSDesktopFolderUsageDescription",
585
+ };
586
+
587
+ // Helper function to escape XML special characters
588
+ function escapeXml(str: string): string {
589
+ return str
590
+ .replace(/&/g, '&amp;')
591
+ .replace(/</g, '&lt;')
592
+ .replace(/>/g, '&gt;')
593
+ .replace(/"/g, '&quot;')
594
+ .replace(/'/g, '&apos;');
595
+ }
596
+
597
+ // Helper function to generate usage description entries for Info.plist
598
+ function generateUsageDescriptions(entitlements: Record<string, boolean | string>): string {
599
+ const usageEntries: string[] = [];
600
+
601
+ for (const [entitlement, value] of Object.entries(entitlements)) {
602
+ const plistKey = ENTITLEMENT_TO_PLIST_KEY[entitlement];
603
+ if (plistKey && value) {
604
+ // Use the string value as description, or a default if it's just true
605
+ const description = typeof value === "string"
606
+ ? escapeXml(value)
607
+ : `This app requires access for ${entitlement.split('.').pop()?.replace('-', ' ')}`;
608
+
609
+ usageEntries.push(` <key>${plistKey}</key>\n <string>${description}</string>`);
610
+ }
611
+ }
612
+
613
+ return usageEntries.join('\n');
614
+ }
615
+
427
616
  const command = commandDefaults[commandArg];
428
617
 
429
618
  if (!command) {
@@ -431,19 +620,21 @@ if (!command) {
431
620
  process.exit(1);
432
621
  }
433
622
 
434
- const config = getConfig();
623
+ // Main execution function
624
+ async function main() {
625
+ const config = await getConfig();
435
626
 
436
- const envArg =
437
- process.argv.find((arg) => arg.startsWith("--env="))?.split("=")[1] || "";
627
+ const envArg =
628
+ process.argv.find((arg) => arg.startsWith("--env="))?.split("=")[1] || "";
438
629
 
439
- const targetsArg =
440
- process.argv.find((arg) => arg.startsWith("--targets="))?.split("=")[1] || "";
630
+ const targetsArg =
631
+ process.argv.find((arg) => arg.startsWith("--targets="))?.split("=")[1] || "";
441
632
 
442
- const validEnvironments = ["dev", "canary", "stable"];
633
+ const validEnvironments = ["dev", "canary", "stable"];
443
634
 
444
- // todo (yoav): dev, canary, and stable;
445
- const buildEnvironment: "dev" | "canary" | "stable" =
446
- validEnvironments.includes(envArg || "dev") ? (envArg || "dev") : "dev";
635
+ // todo (yoav): dev, canary, and stable;
636
+ const buildEnvironment: "dev" | "canary" | "stable" =
637
+ validEnvironments.includes(envArg || "dev") ? (envArg || "dev") : "dev";
447
638
 
448
639
  // Determine build targets
449
640
  type BuildTarget = { os: 'macos' | 'win' | 'linux', arch: 'arm64' | 'x64' };
@@ -678,6 +869,13 @@ function escapePathForTerminal(filePath: string) {
678
869
 
679
870
  return escapedPath;
680
871
  }
872
+
873
+ function sanitizeVolumeNameForHdiutil(volumeName: string) {
874
+ // Remove or replace characters that cause issues with hdiutil volume mounting
875
+ // Parentheses and other special characters can cause "Operation not permitted" errors
876
+ return volumeName.replace(/[()]/g, '');
877
+ }
878
+
681
879
  // MyApp
682
880
 
683
881
  // const appName = config.app.name.replace(/\s/g, '-').toLowerCase();
@@ -696,57 +894,117 @@ const bundleFileName = targetOS === 'macos' ? `${appFileName}.app` : appFileName
696
894
  let proc = null;
697
895
 
698
896
  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);
897
+ await (async () => {
898
+ const secondArg = process.argv[indexOfElectrobun + 2];
899
+ const availableTemplates = getTemplateNames();
732
900
 
733
- // Create directory if it doesn't exist
734
- mkdirSync(dir, { recursive: true });
901
+ let projectName: string;
902
+ let templateName: string;
735
903
 
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!");
904
+ // Check if --template= flag is used
905
+ const templateFlag = process.argv.find(arg => arg.startsWith("--template="));
906
+ if (templateFlag) {
907
+ // Traditional usage: electrobun init my-project --template=photo-booth
908
+ projectName = secondArg || "my-electrobun-app";
909
+ templateName = templateFlag.split("=")[1];
910
+ } else if (secondArg && availableTemplates.includes(secondArg)) {
911
+ // New intuitive usage: electrobun init photo-booth
912
+ projectName = secondArg; // Use template name as project name
913
+ templateName = secondArg;
914
+ } else {
915
+ // Interactive menu when no template specified
916
+ console.log("🚀 Welcome to Electrobun!");
917
+ console.log("");
918
+ console.log("Available templates:");
919
+ availableTemplates.forEach((template, index) => {
920
+ console.log(` ${index + 1}. ${template}`);
921
+ });
922
+ console.log("");
923
+
924
+ // Simple CLI selection using readline
925
+ const rl = readline.createInterface({
926
+ input: process.stdin,
927
+ output: process.stdout
928
+ });
929
+
930
+ const choice = await new Promise<string>((resolve) => {
931
+ rl.question('Select a template (enter number): ', (answer) => {
932
+ rl.close();
933
+ resolve(answer.trim());
934
+ });
935
+ });
936
+
937
+ const templateIndex = parseInt(choice) - 1;
938
+ if (templateIndex < 0 || templateIndex >= availableTemplates.length) {
939
+ console.error(`❌ Invalid selection. Please enter a number between 1 and ${availableTemplates.length}.`);
940
+ process.exit(1);
941
+ }
942
+
943
+ templateName = availableTemplates[templateIndex];
944
+
945
+ // Ask for project name
946
+ const rl2 = readline.createInterface({
947
+ input: process.stdin,
948
+ output: process.stdout
949
+ });
950
+
951
+ projectName = await new Promise<string>((resolve) => {
952
+ rl2.question(`Enter project name (default: my-${templateName}-app): `, (answer) => {
953
+ rl2.close();
954
+ resolve(answer.trim() || `my-${templateName}-app`);
955
+ });
956
+ });
957
+ }
958
+
959
+ console.log(`🚀 Initializing Electrobun project: ${projectName}`);
960
+ console.log(`📋 Using template: ${templateName}`);
961
+
962
+ // Validate template name
963
+ if (!availableTemplates.includes(templateName)) {
964
+ console.error(`❌ Template "${templateName}" not found.`);
965
+ console.log(`Available templates: ${availableTemplates.join(", ")}`);
966
+ process.exit(1);
967
+ }
968
+
969
+ const template = getTemplate(templateName);
970
+ if (!template) {
971
+ console.error(`❌ Could not load template "${templateName}"`);
972
+ process.exit(1);
973
+ }
974
+
975
+ // Create project directory
976
+ const projectPath = join(process.cwd(), projectName);
977
+ if (existsSync(projectPath)) {
978
+ console.error(`❌ Directory "${projectName}" already exists.`);
979
+ process.exit(1);
980
+ }
981
+
982
+ mkdirSync(projectPath, { recursive: true });
983
+
984
+ // Extract template files
985
+ let fileCount = 0;
986
+ for (const [relativePath, content] of Object.entries(template.files)) {
987
+ const fullPath = join(projectPath, relativePath);
988
+ const dir = dirname(fullPath);
989
+
990
+ // Create directory if it doesn't exist
991
+ mkdirSync(dir, { recursive: true });
992
+
993
+ // Write file
994
+ writeFileSync(fullPath, content, 'utf-8');
995
+ fileCount++;
996
+ }
997
+
998
+ console.log(`✅ Created ${fileCount} files from "${templateName}" template`);
999
+ console.log(`📁 Project created at: ${projectPath}`);
1000
+ console.log("");
1001
+ console.log("📦 Next steps:");
1002
+ console.log(` cd ${projectName}`);
1003
+ console.log(" bun install");
1004
+ console.log(" bun start");
1005
+ console.log("");
1006
+ console.log("🎉 Happy building with Electrobun!");
1007
+ })();
750
1008
  } else if (commandArg === "build") {
751
1009
  // Ensure core binaries are available for the target platform before starting build
752
1010
  await ensureCoreDependencies(currentTarget.os, currentTarget.arch);
@@ -792,6 +1050,9 @@ if (commandArg === "init") {
792
1050
 
793
1051
  // We likely want to let users configure this for different environments (eg: dev, canary, stable) and/or
794
1052
  // provide methods to help segment data in those folders based on channel/environment
1053
+ // Generate usage descriptions from entitlements
1054
+ const usageDescriptions = generateUsageDescriptions(config.build.mac.entitlements || {});
1055
+
795
1056
  const InfoPlistContents = `<?xml version="1.0" encoding="UTF-8"?>
796
1057
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
797
1058
  <plist version="1.0">
@@ -807,7 +1068,7 @@ if (commandArg === "init") {
807
1068
  <key>CFBundlePackageType</key>
808
1069
  <string>APPL</string>
809
1070
  <key>CFBundleIconFile</key>
810
- <string>AppIcon</string>
1071
+ <string>AppIcon</string>${usageDescriptions ? '\n' + usageDescriptions : ''}
811
1072
  </dict>
812
1073
  </plist>`;
813
1074
 
@@ -847,23 +1108,21 @@ if (commandArg === "init") {
847
1108
  // mkdirSync(destLauncherFolder, {recursive: true});
848
1109
  // }
849
1110
  // 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
- });
1111
+ // Copy zig launcher for all builds (dev, canary, stable)
1112
+ const bunCliLauncherBinarySource = targetPaths.LAUNCHER_RELEASE;
1113
+ const bunCliLauncherDestination = join(appBundleMacOSPath, "launcher") + targetBinExt;
1114
+ const destLauncherFolder = dirname(bunCliLauncherDestination);
1115
+ if (!existsSync(destLauncherFolder)) {
1116
+ // console.info('creating folder: ', destFolder);
1117
+ mkdirSync(destLauncherFolder, { recursive: true });
864
1118
  }
865
1119
 
866
- cpSync(targetPaths.MAIN_JS, join(appBundleMacOSPath, 'main.js'));
1120
+ cpSync(bunCliLauncherBinarySource, bunCliLauncherDestination, {
1121
+ recursive: true,
1122
+ dereference: true,
1123
+ });
1124
+
1125
+ cpSync(targetPaths.MAIN_JS, join(appBundleFolderResourcesPath, 'main.js'));
867
1126
 
868
1127
  // Bun runtime binary
869
1128
  // todo (yoav): this only works for the current architecture
@@ -1271,11 +1530,13 @@ if (commandArg === "init") {
1271
1530
 
1272
1531
  // Run postBuild script
1273
1532
  if (config.scripts.postBuild) {
1533
+ console.log("Running postBuild script:", config.scripts.postBuild);
1274
1534
  // Use host platform's bun binary for running scripts, not target platform's
1275
1535
  const hostPaths = getPlatformPaths(OS, ARCH);
1276
1536
 
1277
- Bun.spawnSync([hostPaths.BUN_BINARY, config.scripts.postBuild], {
1537
+ const result = Bun.spawnSync([hostPaths.BUN_BINARY, config.scripts.postBuild], {
1278
1538
  stdio: ["ignore", "inherit", "inherit"],
1539
+ cwd: projectRoot, // Add cwd to ensure script runs from project root
1279
1540
  env: {
1280
1541
  ...process.env,
1281
1542
  ELECTROBUN_BUILD_ENV: buildEnvironment,
@@ -1288,6 +1549,18 @@ if (commandArg === "init") {
1288
1549
  ELECTROBUN_ARTIFACT_DIR: artifactFolder,
1289
1550
  },
1290
1551
  });
1552
+
1553
+ if (result.exitCode !== 0) {
1554
+ console.error("postBuild script failed with exit code:", result.exitCode);
1555
+ if (result.stderr) {
1556
+ console.error("stderr:", result.stderr.toString());
1557
+ }
1558
+ // Also log which bun binary we're trying to use
1559
+ console.error("Tried to run with bun at:", hostPaths.BUN_BINARY);
1560
+ console.error("Script path:", config.scripts.postBuild);
1561
+ console.error("Working directory:", projectRoot);
1562
+ process.exit(1);
1563
+ }
1291
1564
  }
1292
1565
  // All the unique files are in the bundle now. Create an initial temporary tar file
1293
1566
  // for hashing the contents
@@ -1476,7 +1749,7 @@ if (commandArg === "init") {
1476
1749
  // hdiutil create -volname "YourAppName" -srcfolder /path/to/YourApp.app -ov -format UDZO YourAppName.dmg
1477
1750
  // Note: use ULFO (lzfse) for better compatibility with large CEF frameworks and modern macOS
1478
1751
  execSync(
1479
- `hdiutil create -volname "${appFileName}" -srcfolder ${escapePathForTerminal(
1752
+ `hdiutil create -volname "${sanitizeVolumeNameForHdiutil(appFileName)}" -srcfolder ${escapePathForTerminal(
1480
1753
  selfExtractingBundle.appBundleFolderPath
1481
1754
  )} -ov -format ULFO ${escapePathForTerminal(dmgPath)}`
1482
1755
  );
@@ -1741,24 +2014,27 @@ exec "\$LAUNCHER_BINARY" "\$@"
1741
2014
 
1742
2015
  let mainProc;
1743
2016
  let bundleExecPath: string;
2017
+ let bundleResourcesPath: string;
1744
2018
 
1745
2019
  if (OS === 'macos') {
1746
2020
  bundleExecPath = join(buildFolder, bundleFileName, "Contents", 'MacOS');
2021
+ bundleResourcesPath = join(buildFolder, bundleFileName, "Contents", 'Resources');
1747
2022
  } else if (OS === 'linux' || OS === 'win') {
1748
2023
  bundleExecPath = join(buildFolder, bundleFileName, "bin");
2024
+ bundleResourcesPath = join(buildFolder, bundleFileName, "Resources");
1749
2025
  } else {
1750
2026
  throw new Error(`Unsupported OS: ${OS}`);
1751
2027
  }
1752
2028
 
1753
2029
  if (OS === 'macos') {
1754
-
1755
- mainProc = Bun.spawn([join(bundleExecPath,'bun'), join(bundleExecPath, 'main.js')], {
2030
+ // Use the zig launcher for all builds (dev, canary, stable)
2031
+ mainProc = Bun.spawn([join(bundleExecPath, 'launcher')], {
1756
2032
  stdio: ['inherit', 'inherit', 'inherit'],
1757
2033
  cwd: bundleExecPath
1758
2034
  })
1759
2035
  } else if (OS === 'win') {
1760
- // Try the main process
1761
- mainProc = Bun.spawn(['./bun.exe', './main.js'], {
2036
+ // Try the main process - use relative path to Resources folder
2037
+ mainProc = Bun.spawn(['./bun.exe', '../Resources/main.js'], {
1762
2038
  stdio: ['inherit', 'inherit', 'inherit'],
1763
2039
  cwd: bundleExecPath,
1764
2040
  onExit: (proc, exitCode, signalCode, error) => {
@@ -1779,7 +2055,7 @@ exec "\$LAUNCHER_BINARY" "\$@"
1779
2055
  }
1780
2056
  }
1781
2057
 
1782
- mainProc = Bun.spawn([join(bundleExecPath, 'bun'), join(bundleExecPath, 'main.js')], {
2058
+ mainProc = Bun.spawn([join(bundleExecPath, 'bun'), join(bundleResourcesPath, 'main.js')], {
1783
2059
  stdio: ['inherit', 'inherit', 'inherit'],
1784
2060
  cwd: bundleExecPath,
1785
2061
  env
@@ -1787,23 +2063,49 @@ exec "\$LAUNCHER_BINARY" "\$@"
1787
2063
  }
1788
2064
 
1789
2065
  process.on("SIGINT", () => {
1790
- console.log('exit command')
1791
- // toLauncherPipe.write("exit command\n");
1792
- mainProc.kill();
1793
- process.exit();
2066
+ console.log('[electrobun dev] Received SIGINT, initiating graceful shutdown...')
2067
+
2068
+ if (mainProc) {
2069
+ // First attempt graceful shutdown by sending SIGINT to child
2070
+ console.log('[electrobun dev] Requesting graceful shutdown from app...')
2071
+ mainProc.kill("SIGINT");
2072
+
2073
+ // Give the app time to clean up (e.g., call killApp())
2074
+ setTimeout(() => {
2075
+ if (mainProc && !mainProc.killed) {
2076
+ console.log('[electrobun dev] App did not exit gracefully, forcing termination...')
2077
+ mainProc.kill("SIGKILL");
2078
+ }
2079
+ process.exit(0);
2080
+ }, 2000); // 2 second timeout for graceful shutdown
2081
+ } else {
2082
+ process.exit(0);
2083
+ }
1794
2084
  });
1795
2085
 
1796
2086
  }
1797
2087
 
1798
- function getConfig() {
2088
+ async function getConfig() {
1799
2089
  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
2090
+ const foundConfigPath = findConfigFile();
2091
+
2092
+ if (foundConfigPath) {
2093
+ console.log(`Using config file: ${basename(foundConfigPath)}`);
2094
+
1803
2095
  try {
1804
- loadedConfig = JSON.parse(configFileContents);
2096
+ // Use dynamic import for TypeScript ESM files
2097
+ // Bun handles TypeScript natively, no transpilation needed
2098
+ const configModule = await import(foundConfigPath);
2099
+ loadedConfig = configModule.default || configModule;
2100
+
2101
+ // Validate that we got a valid config object
2102
+ if (!loadedConfig || typeof loadedConfig !== 'object') {
2103
+ console.error("Config file must export a default object");
2104
+ console.error("using default config instead");
2105
+ loadedConfig = {};
2106
+ }
1805
2107
  } catch (error) {
1806
- console.error("Failed to parse config file:", error);
2108
+ console.error("Failed to load config file:", error);
1807
2109
  console.error("using default config instead");
1808
2110
  }
1809
2111
  }
@@ -1851,7 +2153,7 @@ function getConfig() {
1851
2153
  };
1852
2154
  }
1853
2155
 
1854
- function buildEntitlementsFile(entitlements) {
2156
+ function buildEntitlementsFile(entitlements: Record<string, boolean | string>) {
1855
2157
  return `<?xml version="1.0" encoding="UTF-8"?>
1856
2158
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1857
2159
  <plist version="1.0">
@@ -1870,7 +2172,8 @@ function getEntitlementValue(value: boolean | string) {
1870
2172
  if (typeof value === "boolean") {
1871
2173
  return `<${value.toString()}/>`;
1872
2174
  } else {
1873
- return value;
2175
+ // For string values (usage descriptions), still return boolean true for the entitlement
2176
+ return `<true/>`;
1874
2177
  }
1875
2178
  }
1876
2179
 
@@ -2187,30 +2490,189 @@ function codesignAppBundle(
2187
2490
  process.exit(1);
2188
2491
  }
2189
2492
 
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`
2493
+ // If this is a DMG file, sign it directly
2494
+ if (appBundleOrDmgPath.endsWith('.dmg')) {
2495
+ execSync(
2496
+ `codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" ${escapePathForTerminal(
2497
+ appBundleOrDmgPath
2498
+ )}`
2499
+ );
2500
+ return;
2501
+ }
2195
2502
 
2503
+ // For app bundles, sign binaries individually to avoid --deep issues with notarization
2504
+ const contentsPath = join(appBundleOrDmgPath, 'Contents');
2505
+ const macosPath = join(contentsPath, 'MacOS');
2506
+
2507
+ // Prepare entitlements if provided
2196
2508
  if (entitlementsFilePath) {
2197
2509
  const entitlementsFileContents = buildEntitlementsFile(
2198
2510
  config.build.mac.entitlements
2199
2511
  );
2200
2512
  Bun.write(entitlementsFilePath, entitlementsFileContents);
2513
+ }
2201
2514
 
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
- );
2515
+ // Sign frameworks first (CEF framework requires special handling)
2516
+ const frameworksPath = join(contentsPath, 'Frameworks');
2517
+ if (existsSync(frameworksPath)) {
2518
+ try {
2519
+ const frameworks = readdirSync(frameworksPath);
2520
+ for (const framework of frameworks) {
2521
+ if (framework.endsWith('.framework')) {
2522
+ const frameworkPath = join(frameworksPath, framework);
2523
+
2524
+ if (framework === 'Chromium Embedded Framework.framework') {
2525
+ console.log(`Signing CEF framework components: ${framework}`);
2526
+
2527
+ // Sign CEF libraries first
2528
+ const librariesPath = join(frameworkPath, 'Libraries');
2529
+ if (existsSync(librariesPath)) {
2530
+ const libraries = readdirSync(librariesPath);
2531
+ for (const library of libraries) {
2532
+ if (library.endsWith('.dylib')) {
2533
+ const libraryPath = join(librariesPath, library);
2534
+ console.log(`Signing CEF library: ${library}`);
2535
+ execSync(
2536
+ `codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" --options runtime ${escapePathForTerminal(libraryPath)}`
2537
+ );
2538
+ }
2539
+ }
2540
+ }
2541
+
2542
+ // CEF helper apps are in the main Frameworks directory, not inside the CEF framework
2543
+ // We'll sign them after signing all frameworks
2544
+ }
2545
+
2546
+ // Sign the framework bundle itself (for CEF and any other frameworks)
2547
+ console.log(`Signing framework bundle: ${framework}`);
2548
+ execSync(
2549
+ `codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" --options runtime ${escapePathForTerminal(frameworkPath)}`
2550
+ );
2551
+ }
2552
+ }
2553
+ } catch (err) {
2554
+ console.log("Error signing frameworks:", err);
2555
+ throw err; // Re-throw to fail the build since framework signing is critical
2556
+ }
2213
2557
  }
2558
+
2559
+ // Sign CEF helper apps (they're in the main Frameworks directory, not inside CEF framework)
2560
+ const cefHelperApps = [
2561
+ 'bun Helper.app',
2562
+ 'bun Helper (GPU).app',
2563
+ 'bun Helper (Plugin).app',
2564
+ 'bun Helper (Alerts).app',
2565
+ 'bun Helper (Renderer).app'
2566
+ ];
2567
+
2568
+ for (const helperApp of cefHelperApps) {
2569
+ const helperPath = join(frameworksPath, helperApp);
2570
+ if (existsSync(helperPath)) {
2571
+ const helperExecutablePath = join(helperPath, 'Contents', 'MacOS', helperApp.replace('.app', ''));
2572
+ if (existsSync(helperExecutablePath)) {
2573
+ console.log(`Signing CEF helper executable: ${helperApp}`);
2574
+ const entitlementFlag = entitlementsFilePath ? `--entitlements ${entitlementsFilePath}` : '';
2575
+ execSync(
2576
+ `codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" --options runtime ${entitlementFlag} ${escapePathForTerminal(helperExecutablePath)}`
2577
+ );
2578
+ }
2579
+
2580
+ console.log(`Signing CEF helper bundle: ${helperApp}`);
2581
+ const entitlementFlag = entitlementsFilePath ? `--entitlements ${entitlementsFilePath}` : '';
2582
+ execSync(
2583
+ `codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" --options runtime ${entitlementFlag} ${escapePathForTerminal(helperPath)}`
2584
+ );
2585
+ }
2586
+ }
2587
+
2588
+ // Sign all binaries and libraries in MacOS folder and subdirectories
2589
+ console.log("Signing all binaries in MacOS folder...");
2590
+
2591
+ // Recursively find all executables and libraries in MacOS folder
2592
+ function findExecutables(dir: string): string[] {
2593
+ let executables: string[] = [];
2594
+
2595
+ try {
2596
+ const entries = readdirSync(dir, { withFileTypes: true });
2597
+
2598
+ for (const entry of entries) {
2599
+ const fullPath = join(dir, entry.name);
2600
+
2601
+ if (entry.isDirectory()) {
2602
+ // Recursively search subdirectories
2603
+ executables = executables.concat(findExecutables(fullPath));
2604
+ } else if (entry.isFile()) {
2605
+ // Check if it's an executable or library
2606
+ try {
2607
+ const fileInfo = execSync(`file -b "${fullPath}"`, { encoding: 'utf8' }).trim();
2608
+ if (fileInfo.includes('Mach-O') || entry.name.endsWith('.dylib')) {
2609
+ executables.push(fullPath);
2610
+ }
2611
+ } catch {
2612
+ // If file command fails, check by extension
2613
+ if (entry.name.endsWith('.dylib') || !entry.name.includes('.')) {
2614
+ // No extension often means executable
2615
+ executables.push(fullPath);
2616
+ }
2617
+ }
2618
+ }
2619
+ }
2620
+ } catch (err) {
2621
+ console.error(`Error scanning directory ${dir}:`, err);
2622
+ }
2623
+
2624
+ return executables;
2625
+ }
2626
+
2627
+ const executablesInMacOS = findExecutables(macosPath);
2628
+
2629
+ // Sign each found executable
2630
+ for (const execPath of executablesInMacOS) {
2631
+ const fileName = basename(execPath);
2632
+ const relativePath = execPath.replace(macosPath + '/', '');
2633
+
2634
+ // Use filename as identifier (without extension)
2635
+ const identifier = fileName.replace(/\.[^.]+$/, '');
2636
+
2637
+ console.log(`Signing ${relativePath} with identifier ${identifier}`);
2638
+ const entitlementFlag = entitlementsFilePath ? `--entitlements ${entitlementsFilePath}` : '';
2639
+
2640
+ try {
2641
+ execSync(
2642
+ `codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" --options runtime --identifier ${identifier} ${entitlementFlag} ${escapePathForTerminal(execPath)}`
2643
+ );
2644
+ } catch (err) {
2645
+ console.error(`Failed to sign ${relativePath}:`, err.message);
2646
+ // Continue signing other files even if one fails
2647
+ }
2648
+ }
2649
+
2650
+ // Note: main.js is now in Resources and will be automatically sealed when signing the app bundle
2651
+
2652
+ // Sign the main executable (launcher) - this should use the app's bundle identifier, not "launcher"
2653
+ const launcherPath = join(macosPath, 'launcher');
2654
+ if (existsSync(launcherPath)) {
2655
+ console.log("Signing main executable (launcher)");
2656
+ const entitlementFlag = entitlementsFilePath ? `--entitlements ${entitlementsFilePath}` : '';
2657
+ try {
2658
+ execSync(
2659
+ `codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" --options runtime ${entitlementFlag} ${escapePathForTerminal(launcherPath)}`
2660
+ );
2661
+ } catch (error) {
2662
+ console.error("Failed to sign launcher:", error.message);
2663
+ console.log("Attempting to sign launcher without runtime hardening...");
2664
+ execSync(
2665
+ `codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" ${entitlementFlag} ${escapePathForTerminal(launcherPath)}`
2666
+ );
2667
+ }
2668
+ }
2669
+
2670
+ // Finally, sign the app bundle itself (without --deep)
2671
+ console.log("Signing app bundle");
2672
+ const entitlementFlag = entitlementsFilePath ? `--entitlements ${entitlementsFilePath}` : '';
2673
+ execSync(
2674
+ `codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" --options runtime ${entitlementFlag} ${escapePathForTerminal(appBundleOrDmgPath)}`
2675
+ );
2214
2676
  }
2215
2677
 
2216
2678
  function notarizeAndStaple(appOrDmgPath: string) {
@@ -2349,3 +2811,11 @@ function createAppBundle(bundleName: string, parentFolder: string, targetOS: 'ma
2349
2811
  throw new Error(`Unsupported OS: ${targetOS}`);
2350
2812
  }
2351
2813
  }
2814
+
2815
+ } // End of main() function
2816
+
2817
+ // Run the main function
2818
+ main().catch((error) => {
2819
+ console.error('Fatal error:', error);
2820
+ process.exit(1);
2821
+ });