| 1 | // This file mostly exists because we want dev mode to say "The Knife (Dev)" instead of "electron" |
| 2 | |
| 3 | import * as NodeChildProcess from "node:child_process"; |
| 4 | import * as NodeFS from "node:fs"; |
| 5 | import * as NodeModule from "node:module"; |
| 6 | import * as NodeOS from "node:os"; |
| 7 | import * as NodePath from "node:path"; |
| 8 | import * as NodeURL from "node:url"; |
| 9 | import { ensureElectronRuntime } from "./ensure-electron-runtime.mjs"; |
| 10 | |
| 11 | const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL); |
| 12 | const __dirname = NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)); |
| 13 | export const desktopDir = NodePath.resolve(__dirname, ".."); |
| 14 | const repoRoot = NodePath.resolve(desktopDir, "..", ".."); |
| 15 | const devBundleIdSuffix = NodePath.basename(repoRoot) |
| 16 | .toLowerCase() |
| 17 | .replaceAll(/[^a-z0-9]+/g, ""); |
| 18 | const APP_DISPLAY_NAME = isDevelopment ? "The Knife (Dev)" : "The Knife"; |
| 19 | const APP_BUNDLE_ID = isDevelopment |
| 20 | ? `com.t3tools.t3code.dev.${devBundleIdSuffix || "local"}` |
| 21 | : "com.t3tools.t3code"; |
| 22 | const APP_PROTOCOL_SCHEMES = isDevelopment ? ["t3code-dev"] : ["t3code"]; |
| 23 | const LAUNCHER_VERSION = 20; |
| 24 | const macIconPngPath = NodePath.join(repoRoot, "assets", "knife", "macos-1024.png"); |
| 25 | // oxlint-disable-next-line t3code/no-global-process-runtime -- Standalone launcher script has no Effect runtime. |
| 26 | const hostPlatform = NodeOS.platform(); |
| 27 | |
| 28 | function setPlistString(plistPath, key, value) { |
| 29 | const replaceResult = NodeChildProcess.spawnSync( |
| 30 | "plutil", |
| 31 | ["-replace", key, "-string", value, plistPath], |
| 32 | { |
| 33 | encoding: "utf8", |
| 34 | }, |
| 35 | ); |
| 36 | if (replaceResult.status === 0) { |
| 37 | return; |
| 38 | } |
| 39 | |
| 40 | const insertResult = NodeChildProcess.spawnSync( |
| 41 | "plutil", |
| 42 | ["-insert", key, "-string", value, plistPath], |
| 43 | { |
| 44 | encoding: "utf8", |
| 45 | }, |
| 46 | ); |
| 47 | if (insertResult.status === 0) { |
| 48 | return; |
| 49 | } |
| 50 | |
| 51 | const details = [replaceResult.stderr, insertResult.stderr].filter(Boolean).join("\n"); |
| 52 | throw new Error(`Failed to update plist key "${key}" at ${plistPath}: ${details}`.trim()); |
| 53 | } |
| 54 | |
| 55 | function setPlistJson(plistPath, key, value) { |
| 56 | const serialized = JSON.stringify(value); |
| 57 | const replaceResult = NodeChildProcess.spawnSync( |
| 58 | "plutil", |
| 59 | ["-replace", key, "-json", serialized, plistPath], |
| 60 | { |
| 61 | encoding: "utf8", |
| 62 | }, |
| 63 | ); |
| 64 | if (replaceResult.status === 0) { |
| 65 | return; |
| 66 | } |
| 67 | |
| 68 | const insertResult = NodeChildProcess.spawnSync( |
| 69 | "plutil", |
| 70 | ["-insert", key, "-json", serialized, plistPath], |
| 71 | { |
| 72 | encoding: "utf8", |
| 73 | }, |
| 74 | ); |
| 75 | if (insertResult.status === 0) { |
| 76 | return; |
| 77 | } |
| 78 | |
| 79 | const details = [replaceResult.stderr, insertResult.stderr].filter(Boolean).join("\n"); |
| 80 | throw new Error(`Failed to update plist key "${key}" at ${plistPath}: ${details}`.trim()); |
| 81 | } |
| 82 | |
| 83 | function runChecked(command, args) { |
| 84 | const result = NodeChildProcess.spawnSync(command, args, { encoding: "utf8" }); |
| 85 | if (result.status === 0) { |
| 86 | return; |
| 87 | } |
| 88 | |
| 89 | const details = [result.stdout, result.stderr].filter(Boolean).join("\n"); |
| 90 | throw new Error(`Failed to run ${command} ${args.join(" ")}: ${details}`.trim()); |
| 91 | } |
| 92 | |
| 93 | export function resolveMacCodeSignArguments(appBundlePath) { |
| 94 | return ["--force", "--deep", "--sign", "-", "--timestamp=none", appBundlePath]; |
| 95 | } |
| 96 | |
| 97 | function signMacLauncherBundle(appBundlePath) { |
| 98 | runChecked("codesign", resolveMacCodeSignArguments(appBundlePath)); |
| 99 | } |
| 100 | |
| 101 | function shellSingleQuote(value) { |
| 102 | return `'${value.replaceAll("'", "'\\''")}'`; |
| 103 | } |
| 104 | |
| 105 | export function makeDevelopmentEnvironmentScript(environment) { |
| 106 | const envEntries = [ |
| 107 | ["VITE_DEV_SERVER_URL", environment.VITE_DEV_SERVER_URL], |
| 108 | ["T3CODE_PORT", environment.T3CODE_PORT], |
| 109 | ["T3CODE_HOME", environment.T3CODE_HOME], |
| 110 | ["T3CODE_COMMIT_HASH", environment.T3CODE_COMMIT_HASH], |
| 111 | ["T3CODE_OTLP_TRACES_URL", environment.T3CODE_OTLP_TRACES_URL], |
| 112 | ["T3CODE_OTLP_EXPORT_INTERVAL_MS", environment.T3CODE_OTLP_EXPORT_INTERVAL_MS], |
| 113 | ["T3CODE_OTLP_HEADERS", environment.T3CODE_OTLP_HEADERS], |
| 114 | ["T3CODE_OTLP_PROTOCOL", environment.T3CODE_OTLP_PROTOCOL], |
| 115 | ["T3CODE_DESKTOP_APP_USER_MODEL_ID", APP_BUNDLE_ID], |
| 116 | ].filter((entry) => typeof entry[1] === "string" && entry[1].trim().length > 0); |
| 117 | return [ |
| 118 | ...envEntries.map( |
| 119 | ([name, value]) => |
| 120 | `if [ -z "\${${name}:-}" ]; then export ${name}=${shellSingleQuote(value)}; fi`, |
| 121 | ), |
| 122 | "", |
| 123 | ].join("\n"); |
| 124 | } |
| 125 | |
| 126 | export function makeDevelopmentLauncherScript({ |
| 127 | electronBinaryPath, |
| 128 | mainEntryPath, |
| 129 | desktopRoot, |
| 130 | environmentFilePath, |
| 131 | }) { |
| 132 | return [ |
| 133 | "#!/bin/sh", |
| 134 | `if [ -f ${shellSingleQuote(environmentFilePath)} ]; then . ${shellSingleQuote(environmentFilePath)}; fi`, |
| 135 | `exec ${shellSingleQuote(electronBinaryPath)} --t3code-dev-root=${shellSingleQuote(desktopRoot)} ${shellSingleQuote(mainEntryPath)} "$@"`, |
| 136 | "", |
| 137 | ].join("\n"); |
| 138 | } |
| 139 | |
| 140 | const developmentEnvironmentFilePath = NodePath.join( |
| 141 | desktopDir, |
| 142 | ".electron-runtime", |
| 143 | "dev-environment.sh", |
| 144 | ); |
| 145 | |
| 146 | function writeDevelopmentEnvironmentScript() { |
| 147 | NodeFS.mkdirSync(NodePath.dirname(developmentEnvironmentFilePath), { recursive: true }); |
| 148 | NodeFS.writeFileSync( |
| 149 | developmentEnvironmentFilePath, |
| 150 | makeDevelopmentEnvironmentScript(process.env), |
| 151 | ); |
| 152 | } |
| 153 | |
| 154 | export function writeDevelopmentLauncherScript(targetBinaryPath, electronBinaryPath) { |
| 155 | const script = makeDevelopmentLauncherScript({ |
| 156 | electronBinaryPath, |
| 157 | mainEntryPath: NodePath.join(desktopDir, "dist-electron", "main.cjs"), |
| 158 | desktopRoot: desktopDir, |
| 159 | environmentFilePath: developmentEnvironmentFilePath, |
| 160 | }); |
| 161 | if ( |
| 162 | NodeFS.existsSync(targetBinaryPath) && |
| 163 | NodeFS.readFileSync(targetBinaryPath, "utf8") === script |
| 164 | ) { |
| 165 | NodeFS.chmodSync(targetBinaryPath, 0o755); |
| 166 | return false; |
| 167 | } |
| 168 | NodeFS.writeFileSync(targetBinaryPath, script); |
| 169 | NodeFS.chmodSync(targetBinaryPath, 0o755); |
| 170 | return true; |
| 171 | } |
| 172 | |
| 173 | function registerMacLauncherBundle(appBundlePath) { |
| 174 | runChecked( |
| 175 | "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister", |
| 176 | ["-f", appBundlePath], |
| 177 | ); |
| 178 | |
| 179 | if (!isDevelopment) { |
| 180 | return; |
| 181 | } |
| 182 | |
| 183 | for (const scheme of APP_PROTOCOL_SCHEMES) { |
| 184 | runChecked("osascript", [ |
| 185 | "-l", |
| 186 | "JavaScript", |
| 187 | "-e", |
| 188 | [ |
| 189 | 'ObjC.import("CoreServices");', |
| 190 | `const scheme = $.NSString.alloc.initWithUTF8String(${JSON.stringify(scheme)});`, |
| 191 | `const bundle = $.NSString.alloc.initWithUTF8String(${JSON.stringify(APP_BUNDLE_ID)});`, |
| 192 | "const status = $.LSSetDefaultHandlerForURLScheme(scheme, bundle);", |
| 193 | "if (status !== 0) throw new Error(`LSSetDefaultHandlerForURLScheme failed: ${status}`);", |
| 194 | ].join(" "), |
| 195 | ]); |
| 196 | } |
| 197 | } |
| 198 | |
| 199 | // Bundle-internal paths are macOS paths whatever host builds them. |
| 200 | export function resolveMacLauncherIconPaths(runtimeDir) { |
| 201 | return { |
| 202 | sourceIconPath: macIconPngPath, |
| 203 | generatedIconPath: NodePath.posix.join(runtimeDir, "icon.icns"), |
| 204 | }; |
| 205 | } |
| 206 | |
| 207 | function ensureMacIconIcns(runtimeDir) { |
| 208 | const { sourceIconPath, generatedIconPath } = resolveMacLauncherIconPaths(runtimeDir); |
| 209 | NodeFS.mkdirSync(runtimeDir, { recursive: true }); |
| 210 | |
| 211 | if (!NodeFS.existsSync(sourceIconPath)) { |
| 212 | throw new Error(`Desktop macOS icon source is missing at ${sourceIconPath}`); |
| 213 | } |
| 214 | |
| 215 | const sourceMtimeMs = NodeFS.statSync(sourceIconPath).mtimeMs; |
| 216 | if ( |
| 217 | NodeFS.existsSync(generatedIconPath) && |
| 218 | NodeFS.statSync(generatedIconPath).mtimeMs >= sourceMtimeMs |
| 219 | ) { |
| 220 | return generatedIconPath; |
| 221 | } |
| 222 | |
| 223 | const iconsetRoot = NodeFS.mkdtempSync(NodePath.join(runtimeDir, "dev-iconset-")); |
| 224 | const iconsetDir = NodePath.join(iconsetRoot, "icon.iconset"); |
| 225 | NodeFS.mkdirSync(iconsetDir, { recursive: true }); |
| 226 | |
| 227 | try { |
| 228 | for (const size of [16, 32, 128, 256, 512]) { |
| 229 | runChecked("sips", [ |
| 230 | "-z", |
| 231 | String(size), |
| 232 | String(size), |
| 233 | sourceIconPath, |
| 234 | "--out", |
| 235 | NodePath.join(iconsetDir, `icon_${size}x${size}.png`), |
| 236 | ]); |
| 237 | |
| 238 | const retinaSize = size * 2; |
| 239 | runChecked("sips", [ |
| 240 | "-z", |
| 241 | String(retinaSize), |
| 242 | String(retinaSize), |
| 243 | sourceIconPath, |
| 244 | "--out", |
| 245 | NodePath.join(iconsetDir, `icon_${size}x${size}@2x.png`), |
| 246 | ]); |
| 247 | } |
| 248 | |
| 249 | runChecked("iconutil", ["-c", "icns", iconsetDir, "-o", generatedIconPath]); |
| 250 | return generatedIconPath; |
| 251 | } finally { |
| 252 | NodeFS.rmSync(iconsetRoot, { recursive: true, force: true }); |
| 253 | } |
| 254 | } |
| 255 | |
| 256 | export function resolveMacBundleInfoPlistStrings(executableName) { |
| 257 | return { |
| 258 | CFBundleDisplayName: APP_DISPLAY_NAME, |
| 259 | CFBundleName: APP_DISPLAY_NAME, |
| 260 | CFBundleIdentifier: APP_BUNDLE_ID, |
| 261 | CFBundleExecutable: executableName, |
| 262 | CFBundleIconFile: "icon.icns", |
| 263 | NSScreenCaptureUsageDescription: |
| 264 | "The Knife captures the active window when you use the snapshot shortcut.", |
| 265 | NSDocumentsFolderUsageDescription: "The Knife reads project files you open in the desktop app.", |
| 266 | }; |
| 267 | } |
| 268 | |
| 269 | function patchMainBundleInfoPlist(appBundlePath, iconPath, executableName) { |
| 270 | const infoPlistPath = NodePath.join(appBundlePath, "Contents", "Info.plist"); |
| 271 | for (const [key, value] of Object.entries(resolveMacBundleInfoPlistStrings(executableName))) { |
| 272 | setPlistString(infoPlistPath, key, value); |
| 273 | } |
| 274 | // macOS 26 offers Messages/Mail codes in every text field unless opted out. |
| 275 | setPlistJson(infoPlistPath, "NSAutoFillRequiresTextContentTypeForOneTimeCodeOnMac", true); |
| 276 | setPlistJson(infoPlistPath, "CFBundleURLTypes", [ |
| 277 | { |
| 278 | CFBundleURLName: APP_BUNDLE_ID, |
| 279 | CFBundleURLSchemes: APP_PROTOCOL_SCHEMES, |
| 280 | }, |
| 281 | ]); |
| 282 | |
| 283 | const resourcesDir = NodePath.join(appBundlePath, "Contents", "Resources"); |
| 284 | NodeFS.copyFileSync(iconPath, NodePath.join(resourcesDir, "icon.icns")); |
| 285 | NodeFS.copyFileSync(iconPath, NodePath.join(resourcesDir, "electron.icns")); |
| 286 | } |
| 287 | |
| 288 | function patchHelperBundleInfoPlists(appBundlePath) { |
| 289 | const helperBundleNames = [ |
| 290 | ["Electron Helper.app", "helper", `${APP_DISPLAY_NAME} Helper`], |
| 291 | ["Electron Helper (GPU).app", "helper.gpu", `${APP_DISPLAY_NAME} Helper (GPU)`], |
| 292 | ["Electron Helper (Plugin).app", "helper.plugin", `${APP_DISPLAY_NAME} Helper (Plugin)`], |
| 293 | ["Electron Helper (Renderer).app", "helper.renderer", `${APP_DISPLAY_NAME} Helper (Renderer)`], |
| 294 | ]; |
| 295 | |
| 296 | for (const [bundleName, bundleIdentifierSuffix, bundleDisplayName] of helperBundleNames) { |
| 297 | const infoPlistPath = NodePath.join( |
| 298 | appBundlePath, |
| 299 | "Contents", |
| 300 | "Frameworks", |
| 301 | bundleName, |
| 302 | "Contents", |
| 303 | "Info.plist", |
| 304 | ); |
| 305 | if (!NodeFS.existsSync(infoPlistPath)) { |
| 306 | continue; |
| 307 | } |
| 308 | |
| 309 | setPlistString(infoPlistPath, "CFBundleDisplayName", bundleDisplayName); |
| 310 | setPlistString(infoPlistPath, "CFBundleName", bundleDisplayName); |
| 311 | setPlistString( |
| 312 | infoPlistPath, |
| 313 | "CFBundleIdentifier", |
| 314 | `${APP_BUNDLE_ID}.${bundleIdentifierSuffix}`, |
| 315 | ); |
| 316 | } |
| 317 | } |
| 318 | |
| 319 | function readJson(path) { |
| 320 | try { |
| 321 | return JSON.parse(NodeFS.readFileSync(path, "utf8")); |
| 322 | } catch { |
| 323 | return null; |
| 324 | } |
| 325 | } |
| 326 | |
| 327 | export function resolveMacLauncherPaths(appBundlePath, displayName = APP_DISPLAY_NAME) { |
| 328 | const executableDir = NodePath.posix.join(appBundlePath, "Contents", "MacOS"); |
| 329 | const launcherExecutableName = `${displayName} Launcher`; |
| 330 | return { |
| 331 | launcherExecutableName, |
| 332 | launcherBinaryPath: NodePath.posix.join(executableDir, launcherExecutableName), |
| 333 | runtimeElectronBinaryPath: NodePath.posix.join(executableDir, "Electron"), |
| 334 | }; |
| 335 | } |
| 336 | |
| 337 | function buildMacLauncher(electronBinaryPath) { |
| 338 | const sourceAppBundlePath = NodePath.resolve(NodePath.dirname(electronBinaryPath), "../.."); |
| 339 | const runtimeDir = NodePath.join(desktopDir, ".electron-runtime"); |
| 340 | const targetAppBundlePath = NodePath.join(runtimeDir, `${APP_DISPLAY_NAME}.app`); |
| 341 | const developmentPaths = resolveMacLauncherPaths(targetAppBundlePath); |
| 342 | const runtimeElectronBinaryPath = developmentPaths.runtimeElectronBinaryPath; |
| 343 | const launcherBinaryPath = isDevelopment |
| 344 | ? developmentPaths.launcherBinaryPath |
| 345 | : runtimeElectronBinaryPath; |
| 346 | const iconPath = ensureMacIconIcns(runtimeDir); |
| 347 | const metadataPath = NodePath.join(runtimeDir, "metadata.json"); |
| 348 | |
| 349 | NodeFS.mkdirSync(runtimeDir, { recursive: true }); |
| 350 | |
| 351 | const expectedMetadata = { |
| 352 | launcherVersion: LAUNCHER_VERSION, |
| 353 | sourceAppBundlePath, |
| 354 | sourceAppMtimeMs: NodeFS.statSync(sourceAppBundlePath).mtimeMs, |
| 355 | iconMtimeMs: NodeFS.statSync(iconPath).mtimeMs, |
| 356 | appBundleId: APP_BUNDLE_ID, |
| 357 | appProtocolSchemes: APP_PROTOCOL_SCHEMES, |
| 358 | }; |
| 359 | |
| 360 | const currentMetadata = readJson(metadataPath); |
| 361 | if ( |
| 362 | NodeFS.existsSync(launcherBinaryPath) && |
| 363 | (!isDevelopment || NodeFS.existsSync(runtimeElectronBinaryPath)) && |
| 364 | currentMetadata && |
| 365 | JSON.stringify(currentMetadata) === JSON.stringify(expectedMetadata) |
| 366 | ) { |
| 367 | if (isDevelopment) { |
| 368 | // The launcher also handles protocol activations outside the dev runner, |
| 369 | // so refresh its fallback environment on every launch. Never let a value |
| 370 | // captured by an older parent app override the live dev-runner environment. |
| 371 | writeDevelopmentEnvironmentScript(); |
| 372 | if (writeDevelopmentLauncherScript(launcherBinaryPath, runtimeElectronBinaryPath)) { |
| 373 | signMacLauncherBundle(targetAppBundlePath); |
| 374 | } |
| 375 | } |
| 376 | registerMacLauncherBundle(targetAppBundlePath); |
| 377 | return launcherBinaryPath; |
| 378 | } |
| 379 | |
| 380 | NodeFS.rmSync(targetAppBundlePath, { recursive: true, force: true }); |
| 381 | // verbatimSymlinks keeps the framework's relative symlinks intact |
| 382 | // (e.g. Resources -> Versions/Current/Resources). Without it cpSync |
| 383 | // rewrites them to absolute paths into node_modules, which escape the |
| 384 | // bundle and crash sandboxed helper processes (icudtl.dat not found). |
| 385 | NodeFS.cpSync(sourceAppBundlePath, targetAppBundlePath, { |
| 386 | recursive: true, |
| 387 | verbatimSymlinks: true, |
| 388 | }); |
| 389 | patchMainBundleInfoPlist( |
| 390 | targetAppBundlePath, |
| 391 | iconPath, |
| 392 | isDevelopment ? developmentPaths.launcherExecutableName : "Electron", |
| 393 | ); |
| 394 | patchHelperBundleInfoPlists(targetAppBundlePath); |
| 395 | if (isDevelopment) { |
| 396 | // Keep Electron's native executable inside the branded bundle. Launching the |
| 397 | // node_modules copy makes macOS associate the process (and Dock label) with |
| 398 | // Electron.app even though this bundle's Info.plist names The Knife. |
| 399 | // Its conventional executable name also keeps Electron's default-app runtime |
| 400 | // in development mode instead of making app.isPackaged report true. |
| 401 | writeDevelopmentEnvironmentScript(); |
| 402 | writeDevelopmentLauncherScript(launcherBinaryPath, runtimeElectronBinaryPath); |
| 403 | } |
| 404 | signMacLauncherBundle(targetAppBundlePath); |
| 405 | NodeFS.writeFileSync(metadataPath, `${JSON.stringify(expectedMetadata, null, 2)}\n`); |
| 406 | registerMacLauncherBundle(targetAppBundlePath); |
| 407 | |
| 408 | return launcherBinaryPath; |
| 409 | } |
| 410 | |
| 411 | function isLinuxSetuidSandboxConfigured(electronBinaryPath) { |
| 412 | if (hostPlatform !== "linux") { |
| 413 | return true; |
| 414 | } |
| 415 | |
| 416 | const sandboxPath = NodePath.join(NodePath.dirname(electronBinaryPath), "chrome-sandbox"); |
| 417 | try { |
| 418 | const sandboxStat = NodeFS.statSync(sandboxPath); |
| 419 | return sandboxStat.uid === 0 && (sandboxStat.mode & 0o4777) === 0o4755; |
| 420 | } catch { |
| 421 | return false; |
| 422 | } |
| 423 | } |
| 424 | |
| 425 | function resolveLinuxSandboxArgs(electronBinaryPath) { |
| 426 | if (isLinuxSetuidSandboxConfigured(electronBinaryPath)) { |
| 427 | return []; |
| 428 | } |
| 429 | |
| 430 | console.warn( |
| 431 | "[desktop-launcher] Electron chrome-sandbox is not root-owned with mode 4755; launching local Electron with --no-sandbox.", |
| 432 | ); |
| 433 | return ["--no-sandbox"]; |
| 434 | } |
| 435 | |
| 436 | function resolveElectronPath() { |
| 437 | const electronBinaryPath = resolveElectronBinaryPath(); |
| 438 | |
| 439 | if (hostPlatform !== "darwin") { |
| 440 | return electronBinaryPath; |
| 441 | } |
| 442 | |
| 443 | return buildMacLauncher(electronBinaryPath); |
| 444 | } |
| 445 | |
| 446 | export function resolveElectronLaunchCommand(args = []) { |
| 447 | const electronPath = resolveElectronPath(); |
| 448 | return { |
| 449 | electronPath, |
| 450 | args: [...resolveLinuxSandboxArgs(electronPath), ...args], |
| 451 | }; |
| 452 | } |
| 453 | |
| 454 | export function resolveElectronBinaryPath({ |
| 455 | ensureRuntime = ensureElectronRuntime, |
| 456 | createRequire = NodeModule.createRequire, |
| 457 | moduleUrl = import.meta.url, |
| 458 | } = {}) { |
| 459 | ensureRuntime(); |
| 460 | |
| 461 | const require = createRequire(moduleUrl); |
| 462 | return require("electron"); |
| 463 | } |
| 464 | |
| 465 | export function resolveDevProtocolClient() { |
| 466 | if (hostPlatform !== "darwin" || !isDevelopment) { |
| 467 | return null; |
| 468 | } |
| 469 | |
| 470 | const electronBinaryPath = resolveElectronBinaryPath(); |
| 471 | const launcherBinaryPath = buildMacLauncher(electronBinaryPath); |
| 472 | return { |
| 473 | appBundlePath: NodePath.resolve(launcherBinaryPath, "..", "..", ".."), |
| 474 | appBundleId: APP_BUNDLE_ID, |
| 475 | }; |
| 476 | } |