fix: rename desktop app to OpenHands Agent Canvas (#16353)

This commit is contained in:
Hiep Le
2026-08-06 11:56:13 +00:00
committed by GitHub
parent 31b94014e0
commit 9a023b28bf
10 changed files with 295 additions and 19 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ name: Desktop (macOS)
# The app is only ad-hoc signed (no signing certs exist for any platform);
# a downloaded DMG is quarantined by Gatekeeper, which reports the app as
# "damaged" until the attribute is cleared:
# xattr -d com.apple.quarantine "/Applications/Agent Canvas.app"
# xattr -d com.apple.quarantine "/Applications/OpenHands Agent Canvas.app"
# Intel DMGs are not produced — the bundled uv/node runtimes are host-arch
# only, so Intel users build from source.
on:
+1 -1
View File
@@ -14,5 +14,5 @@ jobs:
secrets: inherit
uses: OpenHands/release-actions/.github/workflows/release-ready.yml@main
with:
product-name: Agent Canvas
product-name: OpenHands Agent Canvas
slack-channel: '#proj-agent-canvas'
+2
View File
@@ -617,6 +617,8 @@ When adding code that needs a new string, decide up front which rule it falls un
- Electron desktop packaging — `electron-builder.config.mjs` uses `directories.app: "electron"` so electron/package.json is the app manifest. Even though electron/package.json has zero `dependencies`, app-builder-lib's `collectNodeModulesWithLogging` walks UP from the app dir looking for the first npm workspace that resolves modules. The next dir in line is the project root, where `npm list --json` reports the full hoisted tree (~342 dirs, ~600 MB of Vite/React/Monaco/HeroUI), and electron-builder copies all of it into `Resources/app/node_modules/`. The walk is hardcoded in `app-builder-lib/out/util/appFileCopier.js::collectNodeModulesWithLogging` — there is no config knob to disable it. Creating an empty `electron/node_modules/` does NOT help because the collector falls through to project root when it sees zero deps. **The fix is the `afterPack` hook** (`stripBundledNodeModules` in `electron-builder.config.mjs`): after electron-builder copies everything, the hook `rm -rf`s `Resources/app/node_modules/` (handling macOS `.app` bundle layout and Linux/Windows flat resources/ layout), then copies back the dependency closure of `RUNTIME_PACKAGES` (`sirv` for static-server.mjs, `httpxy` for proxy-utils.mjs/ingress.mjs — ~200 KB total). Effect: `resources/app/` drops from ~598 MB to ~7 MB; total `linux-unpacked/` from ~1 GB to ~365 MB (the rest is Electron + Chromium + the bundled `uv` binary). If a spawned backend script gains a new bare npm import, add the package to `RUNTIME_PACKAGES` — otherwise that service crashes with `ERR_MODULE_NOT_FOUND` only in the installed app. **Testing trap:** an app launched from `dist-electron/` inside the repo resolves bare specifiers against the repo's own `node_modules` (Node ESM resolution walks up from the script file), so a missing runtime package is invisible there — verify packaged builds from a copy outside the repo tree (e.g. `/Applications`). Don't add real deps to electron/package.json — any real dep would survive the strip and would also have to be hand-installed inside electron/ since the project root is npm-hoisted. `build:desktop:universal` and `--linux/--win/--mac` variants all run the same hook.
- Electron desktop app name in dev (macOS) — `npm run desktop` shows the app as "Electron" in the Dock unless `scripts/brand-dev-electron.mjs` (wired as the `predesktop` hook) has run. There are **three independent name sources** and they must all be set; getting one wrong looks like the fix silently not working. (1) `app.name` — Electron-internal, drives the menu bar, About panel and `app.getPath("userData")`. It comes from `productName` in `electron/package.json`, read by Electron's `default_app` in dev and `lib/browser/init` when packaged. Note `default_app` only reads `<arg>/package.json`, so `npm run desktop` must point electron at the `electron/` **directory** — `electron electron/main.mjs` makes it probe `electron/main.mjs/package.json`, miss, and leave `app.name` at the host bundle default. (2) `CFBundleDisplayName` / `CFBundleName` in the running bundle's Info.plist — what `lsappinfo` and `NSRunningApplication.localizedName` report. (3) **The `.app` directory name — this is what the Dock tooltip actually shows.** macOS prefers the bundle's filesystem name over the plist keys; `/Applications/DBeaver.app` displays as "DBeaver" despite `CFBundleName = "DBeaver Community"`. So patching only the plist is NOT enough — the script also renames `node_modules/electron/dist/Electron.app` → `<productName>.app` and rewrites `node_modules/electron/path.txt` to match (`getElectronPath()` in `node_modules/electron/index.js` joins path.txt onto `dist/` and silently re-downloads Electron ~100 MB if it doesn't resolve, so the two must move together). `CFBundleExecutable` is deliberately left as `Electron` — `/Applications/Antigravity.app` ships that exact value and still displays correctly, so it only affects `ps`/Activity Monitor. Editing the plist does not break code signing: Electron's dist is ad-hoc *linker-signed* (`Info.plist=not bound`, `Sealed Resources=none`), so the signature covers only the Mach-O. `npm run build:desktop` is unaffected by the rename — electron-builder packages from `~/Library/Caches/electron/electron-v*.zip`, never from `node_modules/electron/dist`. The packaged app never had the problem: electron-builder emits `<productName>.app` with matching plist keys. Already-running instances keep the name they launched with, so quit and relaunch when verifying.
- Electron desktop `node` / `npm` / `npx` PATH bridging — when the packaged `.app` is launched from Finder/Spotlight on macOS, the OS gives it a minimal PATH (`/usr/bin:/bin`). Homebrew, nvm, asdf installs of Node.js are invisible to spawned subprocesses. Two breakages flow from that: (1) backend launcher scripts that do `spawn("node", ...)` can't find Node; (2) most stdio MCP marketplace entries (Slack, GitHub, Figma, etc.) use `command: "npx"`, and when the agent-server tries to spawn them the missing `npx` makes the spawn fail with ENOENT — the SDK reports it as an `error_kind: "connection"` MCP test failure, which the install modal renders as `MCP$TEST_ERROR_CONNECTION` ("Could not reach the server. Check the URL and server type."), a misleading error since no URL is involved. **First fix attempt — DOES NOT WORK for stdio MCPs:** wrap `node`/`npm`/`npx` with thin shell scripts that run Electron with `ELECTRON_RUN_AS_NODE=1` against the package's CLI JS. That bridges the ENOENT but stdio JSON-RPC servers spawned through the wrapper exit with `McpError: Connection closed` before completing the MCP handshake — Electron-as-Node has subtly different stdin/stdout pipe semantics from a vanilla `node` binary when used as a stdio child of a windowed process. **Working fix:** bundle the real Node.js distribution. `scripts/download-node.mjs` downloads the official `node-v<ver>-<platform>-<arch>` tarball from `https://nodejs.org/dist/v<ver>/` into `resources/node/` (gitignored), prunes `include/`, `share/`, docs, and `node_modules/corepack` to keep the size down (~130 MB on Linux x64, dominated by the Node binary itself). Default pin: `NODE_BUNDLE_VERSION = "22.12.0"` (the repo's `engines.node` floor; every 22.x build shares the Electron 42 ABI); override with `NODE_VERSION=`. `electron-builder.config.mjs` ships `resources/node/` as an extraResource → `<Resources>/node/`. `electron/main.mjs::injectBundledNode()` prepends the platform-appropriate bin dir to `PATH` (POSIX: `<Resources>/node/bin`; Windows: `<Resources>/node/`) so subsequent spawns of `node`/`npm`/`npx` resolve to real binaries with full stdio fidelity. It also `chmod +x`'s the binaries on POSIX because electron-builder doesn't always preserve the bit. `injectBundledNode()` is a no-op when `!app.isPackaged` (dev `npm run desktop` uses the developer's system node). `build:desktop` and `build:desktop:universal` both run `download-node.mjs` after `download-uv.mjs`. If the bundled dir is missing at runtime, `injectBundledNode()` logs a loud `[desktop]` warning instead of silently leaving PATH bare.
- Cloud conversation resume gating: when a cloud conversation is closed from the UI (`pauseCloudSandbox` is called), the conversation's `conversation_url` is NOT cleared -- it still points to the old sandbox host. `WebSocketProviderWrapper` must suppress the URL (pass `null` to `ConversationWebSocketProvider`) while `sandbox_status === "PAUSED"`, otherwise the WebSocket immediately tries the stale URL before the sandbox wakes. Symmetrically, `useActiveConversation`'s refetch interval must fast-poll (3 s) on both `!conversation_url` AND `sandbox_status === "PAUSED"` -- checking only the missing URL would leave the hook on the 30 s interval while the sandbox is resuming. The resume sequence: navigate -> sandbox PAUSED detected -> `resumeCloudSandbox` called (in `conversation.tsx`) -> fast-poll detects RUNNING -> `conversationUrl` unblocked -> WebSocket connects.
@@ -7,7 +7,7 @@
// These tests therefore build a fake bundle under os.tmpdir() and verify
// resolution with a real `node --eval` from that location.
import { spawnSync } from "node:child_process";
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
@@ -15,7 +15,7 @@ import config from "../../electron-builder.config.mjs";
const afterPack = config.afterPack as (ctx: unknown) => Promise<void>;
const PRODUCT_FILENAME = "Agent Canvas";
const PRODUCT_FILENAME = "OpenHands Agent Canvas";
function makeContext(platform: string, appOutDir: string) {
return {
@@ -96,3 +96,23 @@ describe("electron-builder afterPack hook", () => {
expect(existsSync(join(appDir, "node_modules", "httpxy", "package.json"))).toBe(true);
});
});
// The app's display name is set in two places that must agree:
// electron-builder.config.mjs productName → CFBundleName / NSIS / .desktop
// name of the PACKAGED bundle (what the Dock, ⌘-Tab and Finder show).
// electron/package.json productName → app.name at runtime (macOS menu
// bar, About panel, app.getPath("userData")), read by Electron's
// default_app in dev and by lib/browser/init when packaged.
// Neither one implies the other — the packaged app shipped with
// CFBundleName "Agent Canvas" but app.name "agent-canvas" until both were
// set. Pin them together so they can't drift again.
describe("desktop app name", () => {
it("keeps electron/package.json productName in sync with the builder config", () => {
const appManifest = JSON.parse(
readFileSync(join(import.meta.dirname, "../../electron/package.json"), "utf8"),
);
expect(appManifest.productName).toBe(config.productName);
expect(config.productName).toBe(PRODUCT_FILENAME);
});
});
+7 -7
View File
@@ -209,7 +209,7 @@ function getDirSizeBytes(dir) {
/** @type {import('electron-builder').Configuration} */
const config = {
appId: "dev.openhands.agent-canvas",
productName: "Agent Canvas",
productName: "OpenHands Agent Canvas",
copyright: "Copyright © 2025 All Hands AI",
// Stamp the packaged app with the released version (see rootPackageJson
@@ -312,16 +312,16 @@ const config = {
},
dmg: {
title: "Agent Canvas",
title: "OpenHands Agent Canvas",
contents: [
{ x: 130, y: 220 },
{ x: 410, y: 220, type: "link", path: "/Applications" },
],
window: { width: 540, height: 380 },
// Default is "Agent Canvas-<version>-<arch>.dmg"; GitHub release assets
// mangle spaces, so keep the asset name literal (matches the nsis
// Default is "OpenHands Agent Canvas-<version>-<arch>.dmg"; GitHub release
// assets mangle spaces, so keep the asset name literal (matches the nsis
// convention). ${version}/${arch}/${ext} are electron-builder macros.
artifactName: "Agent-Canvas-${version}-${arch}.${ext}",
artifactName: "OpenHands-Agent-Canvas-${version}-${arch}.${ext}",
},
// ── Windows ────────────────────────────────────────────────────────────────
@@ -338,10 +338,10 @@ const config = {
allowToChangeInstallationDirectory: true,
createDesktopShortcut: true,
createStartMenuShortcut: true,
// The default artifact name is "Agent Canvas Setup <version>.exe";
// The default artifact name is "OpenHands Agent Canvas Setup <version>.exe";
// GitHub release assets mangle spaces, so ship a space-free name.
// ${version}/${ext} are electron-builder macros, not JS interpolation.
artifactName: "Agent-Canvas-Setup-${version}.${ext}",
artifactName: "OpenHands-Agent-Canvas-Setup-${version}.${ext}",
},
// ── Linux ──────────────────────────────────────────────────────────────────
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Agent Canvas</title>
<title>OpenHands Agent Canvas</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
@@ -232,7 +232,7 @@
<body>
<div class="container">
<img class="logo" src="build-resources/icon.png" alt="" draggable="false" />
<h1>Agent Canvas</h1>
<h1>OpenHands Agent Canvas</h1>
<p class="tagline">AI coding agent interface</p>
<svg class="spinner" viewBox="0 0 66 66" fill="none" aria-hidden="true"
xmlns="http://www.w3.org/2000/svg">
+13 -5
View File
@@ -17,13 +17,19 @@
* Contents/Resources/bin/ ← process.resourcesPath/bin
* uv uvx ← bundled via extraResources
*
* Dev (npm run desktop → electron electron/main.mjs):
* Dev (npm run desktop → electron electron):
* electron/main.mjs ← __dirname = <repo>/electron/
* scripts/ config/ build/ ← one level up: <repo>/
* system uvx from PATH
*
* When packaged, scripts/config/build are siblings of main.mjs so
* projectRoot === __dirname. In dev they are one level up.
*
* The dev command points electron at the electron/ DIRECTORY, not at
* main.mjs directly. Electron's default_app only reads name/productName/
* version out of <arg>/package.json, so passing the file makes it look for
* electron/main.mjs/package.json, miss, and leave app.name at the host
* bundle's default — "Electron" in the menu bar and userData path.
*/
import {
@@ -47,8 +53,10 @@ const __dirname = dirname(__filename);
// ── Path resolution ───────────────────────────────────────────────────────────
// Packaged (directories.app: 'electron'): scripts/config/build are SIBLINGS of
// main.mjs inside Resources/app/, so projectRoot === __dirname.
// Dev (electron electron/main.mjs): those directories are one level UP in the
// Dev (electron electron): those directories are one level UP in the
// repo root, so projectRoot === join(__dirname, '..').
// Both branches key off __dirname (always <repo>/electron in dev), not
// app.getAppPath(), so the entry-point form doesn't affect them.
const projectRoot = app.isPackaged ? __dirname : join(__dirname, "..");
const buildDir = join(projectRoot, "build");
@@ -597,7 +605,7 @@ async function startStack() {
// onServiceLog: stream uvx/agent-server output to the loading window so
// the user sees progress instead of an indefinite spinner.
const result = await main({
bannerTitle: "Agent Canvas",
bannerTitle: "OpenHands Agent Canvas",
staticMode: true,
staticDir: buildDir,
mode: "agent-canvas",
@@ -641,7 +649,7 @@ app.whenReady().then(async () => {
dialog.showErrorBox(
"Missing prerequisite: uv",
app.isPackaged
? "The bundled uv binary could not be found. Please reinstall Agent Canvas."
? "The bundled uv binary could not be found. Please reinstall OpenHands Agent Canvas."
: "uv (uvx) is not installed.\n\nInstall it from https://docs.astral.sh/uv/ then restart.",
);
app.quit();
@@ -684,7 +692,7 @@ app.whenReady().then(async () => {
const errorTail = recentServiceErrors.length
? `\n\nRecent service errors:\n${recentServiceErrors.join("\n")}`
: "";
dialog.showErrorBox("Agent Canvas failed to start", summary + errorTail);
dialog.showErrorBox("OpenHands Agent Canvas failed to start", summary + errorTail);
app.quit();
}
});
+1
View File
@@ -1,5 +1,6 @@
{
"name": "agent-canvas",
"productName": "OpenHands Agent Canvas",
"version": "1.0.0",
"private": true,
"main": "main.mjs"
+2 -1
View File
@@ -104,7 +104,8 @@
"build:lib": "npm run make-i18n && react-router typegen && cross-env BUILD_LIB=true vite build && tsc -p tsconfig.lib.json",
"build:docker": "node scripts/docker-build.mjs",
"example:acp-docker:env": "node scripts/gen-acp-docker-env.mjs",
"desktop": "npm run build:app && electron electron/main.mjs",
"predesktop": "node scripts/brand-dev-electron.mjs",
"desktop": "npm run build:app && electron electron",
"build:desktop": "npm run build:app && node scripts/download-uv.mjs && node scripts/download-node.mjs && electron-builder --config electron-builder.config.mjs --publish never",
"build:desktop:universal": "npm run build:app && node scripts/download-uv.mjs && node scripts/download-node.mjs && cross-env ELECTRON_ARCH=universal electron-builder --config electron-builder.config.mjs --publish never",
"download-uv": "node scripts/download-uv.mjs",
+244
View File
@@ -0,0 +1,244 @@
#!/usr/bin/env node
/**
* Brand the dev Electron app bundle with the product name (macOS only).
*
* Run automatically as the `predesktop` npm hook.
*
* WHY THIS EXISTS
*
* `npm run desktop` runs the app inside Electron's own prebuilt bundle,
* node_modules/electron/dist/Electron.app. macOS derives the name it shows
* in the Dock tooltip, the ⌘-Tab switcher and Finder from that bundle, at
* launch, before any JavaScript runs. No runtime API can change it —
* `app.setName()`, `app.name` and package.json `productName` only drive
* Electron's own notion of the name (menu bar, About panel, and
* app.getPath("userData")).
*
* The packaged app has never had this problem: electron-builder emits a
* bundle literally named "<productName>.app" with matching plist keys. This
* script puts the dev bundle in that same state.
*
* THE BUNDLE FILENAME IS THE PART THAT ACTUALLY SHOWS
*
* Patching the plist alone is NOT enough — verified the hard way. macOS
* prefers the bundle's filesystem name over CFBundleName/CFBundleDisplayName
* for the Dock tooltip. Two installed apps prove each half of this:
*
* DBeaver.app CFBundleName "DBeaver Community" → displays "DBeaver"
* (the filename wins over the plist)
* Antigravity.app CFBundleExecutable "Electron" → displays "Antigravity"
* (an Electron app whose executable name is irrelevant)
*
* So the fix aligns every source of the name at once: the .app directory
* name, CFBundleName and CFBundleDisplayName. That is exactly the shape of a
* packaged build, which is known to display correctly.
*
* Renaming the bundle means node_modules/electron/path.txt has to move with
* it: getElectronPath() in node_modules/electron/index.js joins path.txt onto
* dist/ and silently re-downloads Electron (~100 MB) if the result does not
* exist. The two are updated together, and the rename is rolled back if
* path.txt cannot be written.
*
* WHY THIS IS SAFE
*
* - Electron's prebuilt dist is ad-hoc *linker-signed*: `codesign -dv`
* reports `flags=0x20002(adhoc,linker-signed)`, `Info.plist=not bound`,
* `Sealed Resources=none`. The signature covers only the Mach-O, so
* editing Info.plist does not invalidate it and no re-signing is needed.
* - `npm run build:desktop` is unaffected: electron-builder packages from
* its own download cache (~/Library/Caches/electron/electron-v*.zip),
* never from node_modules/electron/dist.
*
* WHAT IT DELIBERATELY DOES NOT TOUCH
*
* CFBundleExecutable — left as "Electron". Antigravity above shows it has
* no bearing on the displayed name; it only feeds ps / Activity Monitor.
* Leaving it alone keeps path.txt's trailing segments valid.
* CFBundleIdentifier — kept at com.github.Electron. Changing it would split
* LaunchServices / TCC state per checkout for no visible gain.
*
* The changes live in node_modules, which `npm ci` wipes. That is fine: the
* `predesktop` hook re-applies them on every `npm run desktop`.
*
* This script must never block the desktop run — every failure path warns
* and exits 0.
*/
import { execFileSync } from "node:child_process";
import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
import { createRequire } from "node:module";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const projectRoot = join(__dirname, "..");
// Both keys are set. CFBundleDisplayName is the one LaunchServices reports;
// CFBundleName is the fallback and shows up in other bundle-name surfaces.
const NAME_KEYS = ["CFBundleDisplayName", "CFBundleName"];
function warn(message) {
console.warn(`[brand-dev-electron] ${message}`);
}
/** Root of the installed `electron` package, or null if it isn't resolvable. */
function resolveElectronPackage() {
const require = createRequire(import.meta.url);
return dirname(require.resolve("electron/package.json"));
}
/** Current value of `key`, or null when the key is absent. */
function readPlistString(plistPath, key) {
try {
return execFileSync(
"plutil",
["-extract", key, "raw", "-o", "-", plistPath],
{ encoding: "utf8" },
).trim();
} catch {
return null;
}
}
function writePlistString(plistPath, key, value) {
execFileSync("plutil", ["-replace", key, "-string", value, plistPath], {
stdio: "pipe",
});
}
/**
* Rename dist/<current>.app to dist/<productName>.app and repoint path.txt.
* Returns { appDir, renamed }, or null if the bundle can't be determined.
*/
function ensureBundleName(pkgDir, productName) {
const distDir = join(pkgDir, "dist");
const pathFile = join(pkgDir, "path.txt");
if (!existsSync(pathFile)) {
warn(`No ${pathFile} — leaving the dev bundle alone.`);
return null;
}
// e.g. "Electron.app/Contents/MacOS/Electron" — only the first segment
// (the bundle directory) is ours to rename.
const relative = readFileSync(pathFile, "utf8").trim();
const segments = relative.split("/");
const currentName = segments[0];
const desiredName = `${productName}.app`;
if (!currentName.endsWith(".app")) {
warn(`Unexpected path.txt entry "${relative}" — leaving the bundle alone.`);
return null;
}
const desiredDir = join(distDir, desiredName);
if (currentName === desiredName && existsSync(desiredDir)) {
return { appDir: desiredDir, renamed: false };
}
const currentDir = join(distDir, currentName);
let renamed = false;
if (existsSync(currentDir) && currentDir !== desiredDir) {
if (existsSync(desiredDir)) {
// Both present: a previous run renamed the bundle and something
// restored the original. Prefer the correctly named one and just fix
// path.txt rather than clobbering either bundle.
warn(`Both ${currentName} and ${desiredName} exist — using the latter.`);
} else {
renameSync(currentDir, desiredDir);
renamed = true;
}
} else if (!existsSync(desiredDir)) {
warn(`No Electron bundle under ${distDir} — leaving the dev bundle alone.`);
return null;
}
// path.txt MUST agree with the directory on disk; a stale entry makes
// getElectronPath() re-download Electron on the next run.
segments[0] = desiredName;
try {
writeFileSync(pathFile, segments.join("/"));
} catch (err) {
// Undo the rename so the checkout is left in a working state.
if (existsSync(desiredDir) && !existsSync(currentDir)) {
try {
renameSync(desiredDir, currentDir);
} catch {
warn(`Could not roll back the bundle rename in ${distDir}.`);
}
}
warn(`Could not update ${pathFile}: ${err.message}`);
return null;
}
return { appDir: desiredDir, renamed };
}
function main() {
// The Dock/⌘-Tab name is a macOS bundle concept. On Windows the dev
// taskbar name comes from electron.exe's version resource and on Linux
// from the .desktop file / WM_CLASS — neither exists until the app is
// packaged, so there is nothing to patch.
if (process.platform !== "darwin") return;
const manifestPath = join(projectRoot, "electron", "package.json");
let productName;
try {
productName = JSON.parse(readFileSync(manifestPath, "utf8")).productName;
} catch (err) {
warn(`Could not read ${manifestPath}: ${err.message}`);
return;
}
if (!productName) {
warn(`No productName in ${manifestPath} — leaving the dev bundle alone.`);
return;
}
// The name becomes a directory entry; a "/" would silently retarget it.
if (productName.includes("/")) {
warn(`productName "${productName}" cannot be used as a bundle name.`);
return;
}
let pkgDir;
try {
pkgDir = resolveElectronPackage();
} catch (err) {
warn(`Could not resolve the electron package: ${err.message}`);
return;
}
let bundle;
try {
bundle = ensureBundleName(pkgDir, productName);
} catch (err) {
warn(`Could not rename the dev bundle: ${err.message}`);
return;
}
if (!bundle) return;
const plistPath = join(bundle.appDir, "Contents", "Info.plist");
if (!existsSync(plistPath)) {
warn(`No Info.plist at ${plistPath} — leaving the plist alone.`);
return;
}
const stale = NAME_KEYS.filter(
(key) => readPlistString(plistPath, key) !== productName,
);
try {
for (const key of stale) writePlistString(plistPath, key, productName);
} catch (err) {
// Read-only node_modules (CI caches, sandboxes) lands here. The app still
// runs; only the displayed name keeps saying "Electron".
warn(`Could not patch ${plistPath}: ${err.message}`);
return;
}
// Stay quiet when there was nothing to do, so repeat runs don't add noise.
if (bundle.renamed || stale.length > 0) {
console.log(
`[brand-dev-electron] Dev Electron bundle now identifies as "${productName}".`,
);
}
}
main();