From 3ac23cad3ef159cd2f40f9291a7835e52192a15e Mon Sep 17 00:00:00 2001 From: dallasbpeters Date: Mon, 24 Aug 2026 13:07:47 -0500 Subject: [PATCH] Fix the two reasons the packaged app would not open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v0.0.1 aborted before drawing a window. The first cause was hiding the second. CFBundleName was overridden to "RoleModel Studio". Electron resolves its helper apps from CFBundleName, and electron-builder names them after `productName`, so they ship as "Openscreen Helper.app" — leaving Electron looking for "RoleModel Studio Helper.app", finding nothing, and aborting: FATAL:electron_main_delegate_mac.mm:65] Unable to find helper app Our own cask comments describe this failure, in the note explaining why there is no `binary` stanza. It was reached a different way here, and the diagnosis missed it for a while because the process exited 133 with that single line. Nothing is lost by leaving CFBundleName alone. The menu bar reads "RoleModel Studio" because main.ts calls app.setName(PRODUCT_NAME) at module scope, and Finder, the Dock, the About panel and every permission prompt read CFBundleDisplayName, which stays. With that fixed the app got further and threw: The 'screen' module can't be used before the app 'ready' event `app.on("activate")` called showMainWindow with no readiness guard, and activate can arrive before whenReady — exactly what the open-file handler twelve lines above already guards against. The window it builds measures the primary display, so the throw takes the main process down. Ignoring an early activate costs nothing: whenReady builds the window anyway, and macOS re-sends activate on a dock click. Worth recording how this was mis-diagnosed twice. The first theory was hardened runtime, from `flags=0x10002(adhoc,runtime)` on the release against `0x20002(adhoc,linker-signed)` on the working dev build; re-signing without it changed nothing. The second was a silent single-instance quit, from an exit code of 0 with no output — which was this shell exporting ELECTRON_RUN_AS_NODE=1, so the binary was running as node and reading empty stdin. The real error only appeared with that unset. Co-Authored-By: Claude Opus 5 --- electron-builder.json5 | 15 ++++++++++++++- electron/main.ts | 7 +++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/electron-builder.json5 b/electron-builder.json5 index 1bc4db5d4..33f516d38 100644 --- a/electron-builder.json5 +++ b/electron-builder.json5 @@ -171,7 +171,20 @@ // `app.setName()` in electron/main.ts sets the same string for an unpackaged // run, where there is no Info.plist to read. "CFBundleDisplayName": "RoleModel Studio", - "CFBundleName": "RoleModel Studio", + // CFBundleName is deliberately NOT overridden. + // + // Electron resolves its helper apps from CFBundleName. electron-builder names + // them after `productName`, so they ship as "Openscreen Helper.app" — and + // setting CFBundleName to "RoleModel Studio" made Electron look for + // "RoleModel Studio Helper.app", find nothing, and abort before drawing a + // window: + // + // FATAL:electron_main_delegate_mac.mm:65] Unable to find helper app + // + // That is what made v0.0.1 unopenable. Nothing is lost by leaving it alone: + // the menu bar reads "RoleModel Studio" because electron/main.ts calls + // `app.setName(PRODUCT_NAME)` at module scope, and Finder, the Dock, the + // About panel and every permission prompt read CFBundleDisplayName above. // These are quoted verbatim in the macOS permission dialogs, so they say the // name of the app the person is being asked about. "NSAudioCaptureUsageDescription": "RoleModel Studio needs audio capture permission to record system audio.", diff --git a/electron/main.ts b/electron/main.ts index dee0edc7f..97025bfbe 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1092,6 +1092,13 @@ if (!cliCommand) { app.on("activate", () => { if (cliCommand) return; + // `activate` can arrive before `whenReady`, the same way `open-file` can — and + // the window this ends up building measures the primary display, which throws + // "The 'screen' module can't be used before the app 'ready' event" and takes the + // main process with it. Launching the binary directly is enough to see it. + // Nothing is lost by ignoring an early activate: whenReady creates the window + // anyway, and macOS re-sends activate when the dock icon is clicked. + if (!app.isReady()) return; // On macOS, re-open a window when the dock icon is clicked and none are open. const hasVisibleWindow = BrowserWindow.getAllWindows().some((window) => { if (window.isDestroyed() || !window.isVisible()) {