Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 118 additions & 5 deletions apps/desktop/scripts/patch-dev-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,17 @@
*/

import { execSync } from "node:child_process";
import { existsSync } from "node:fs";
import {
existsSync,
lstatSync,
readFileSync,
readlinkSync,
renameSync,
rmSync,
symlinkSync,
unlinkSync,
writeFileSync,
} from "node:fs";
import { homedir } from "node:os";
import { isAbsolute, relative, resolve, sep } from "node:path";
import { config } from "dotenv";
Expand Down Expand Up @@ -57,17 +67,20 @@ if (!workspaceName) {
}
const PROTOCOL_SCHEME = `superset-${workspaceName}`;
const BUNDLE_ID = `com.superset.desktop.${workspaceName}`;
const ELECTRON_APP_PATH = resolve(
const ELECTRON_DIST_DIR = resolve(
import.meta.dirname,
"../node_modules/electron/dist/Electron.app",
"../node_modules/electron/dist",
);
const ELECTRON_APP_PATH = resolve(ELECTRON_DIST_DIR, "Electron.app");
const PLIST_PATH = resolve(ELECTRON_APP_PATH, "Contents/Info.plist");

if (!existsSync(PLIST_PATH)) {
console.log("[patch-dev-protocol] Electron.app not found, skipping");
process.exit(0);
}

const DISPLAY_NAME = `Superset (${workspaceName})`;

try {
const currentBundleId = execSync(
`/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "${PLIST_PATH}" 2>/dev/null`,
Expand All @@ -77,8 +90,34 @@ try {
`/usr/libexec/PlistBuddy -c "Print :CFBundleURLTypes:0:CFBundleURLSchemes:0" "${PLIST_PATH}" 2>/dev/null`,
{ encoding: "utf-8" },
).trim();
const currentName = execSync(
`/usr/libexec/PlistBuddy -c "Print :CFBundleName" "${PLIST_PATH}" 2>/dev/null`,
{ encoding: "utf-8" },
).trim();

if (currentBundleId === BUNDLE_ID && currentScheme === PROTOCOL_SCHEME) {
// Also check if the .app has been renamed and path.txt is updated
const isRenamed =
lstatSync(ELECTRON_APP_PATH).isSymbolicLink() &&
readlinkSync(ELECTRON_APP_PATH) === `${DISPLAY_NAME}.app`;
const electronPkgCheck = resolve(
import.meta.dirname,
"../node_modules/electron",
);
const pathTxtCheck = resolve(electronPkgCheck, "path.txt");
let pathTxtCorrect = false;
try {
pathTxtCorrect =
readFileSync(pathTxtCheck, "utf-8").trim() ===
`${DISPLAY_NAME}.app/Contents/MacOS/Electron`;
} catch {}

if (
currentBundleId === BUNDLE_ID &&
currentScheme === PROTOCOL_SCHEME &&
currentName === DISPLAY_NAME &&
isRenamed &&
pathTxtCorrect
) {
console.log(
`[patch-dev-protocol] ${PROTOCOL_SCHEME}:// already registered`,
);
Expand All @@ -92,6 +131,21 @@ execSync(
`/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier ${BUNDLE_ID}" "${PLIST_PATH}"`,
);

// CFBundleName exists in default Electron plist, so Set works
execSync(
`/usr/libexec/PlistBuddy -c "Set :CFBundleName ${DISPLAY_NAME}" "${PLIST_PATH}"`,
);

// CFBundleDisplayName may not exist — delete then add to handle both cases
try {
execSync(
`/usr/libexec/PlistBuddy -c "Delete :CFBundleDisplayName" "${PLIST_PATH}" 2>/dev/null`,
);
} catch {}
execSync(
`/usr/libexec/PlistBuddy -c "Add :CFBundleDisplayName string '${DISPLAY_NAME}'" "${PLIST_PATH}"`,
);

// Remove existing URL types to avoid stale entries from previous patches
try {
execSync(
Expand All @@ -112,9 +166,53 @@ for (const cmd of commands) {
execSync(`/usr/libexec/PlistBuddy -c "${cmd}" "${PLIST_PATH}"`);
}

// Rename Electron.app so macOS uses our display name for the dock label.
// The plist CFBundleName is set correctly, but Electron's runtime overrides
// the in-memory value before the dock reads it. Renaming the .app bundle
// ensures macOS sees the correct name from the bundle directory itself.
// A symlink preserves backward compatibility for the `electron` npm package.
const DESIRED_APP_NAME = `${DISPLAY_NAME}.app`;
const desiredAppPath = resolve(ELECTRON_DIST_DIR, DESIRED_APP_NAME);
let actualAppPath = ELECTRON_APP_PATH;

try {
const stats = lstatSync(ELECTRON_APP_PATH);

if (stats.isSymbolicLink()) {
const currentTarget = readlinkSync(ELECTRON_APP_PATH);
if (currentTarget === DESIRED_APP_NAME) {
// Already correctly renamed
actualAppPath = desiredAppPath;
} else {
// Different workspace name from previous run — update
const oldTargetPath = resolve(ELECTRON_DIST_DIR, currentTarget);
unlinkSync(ELECTRON_APP_PATH);
if (existsSync(oldTargetPath)) {
renameSync(oldTargetPath, desiredAppPath);
}
symlinkSync(DESIRED_APP_NAME, ELECTRON_APP_PATH);
actualAppPath = desiredAppPath;
Comment on lines +186 to +194
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Dangling symlink when the previous workspace's app directory is missing.

If oldTargetPath doesn't exist (Line 190 is false), the renameSync is skipped but symlinkSync on Line 193 still creates a symlink pointing to DESIRED_APP_NAME — which won't exist either. This leaves a dangling symlink and the rest of the script (lsregister, path.txt) will operate on a nonexistent path.

🛡️ Suggested fix
 		const oldTargetPath = resolve(ELECTRON_DIST_DIR, currentTarget);
 		unlinkSync(ELECTRON_APP_PATH);
 		if (existsSync(oldTargetPath)) {
 			renameSync(oldTargetPath, desiredAppPath);
+		} else if (!existsSync(desiredAppPath)) {
+			console.warn("[patch-dev-protocol] Previous Electron.app not found and desired target missing — re-install electron");
+			process.exit(1);
 		}
 		symlinkSync(DESIRED_APP_NAME, ELECTRON_APP_PATH);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else {
// Different workspace name from previous run — update
const oldTargetPath = resolve(ELECTRON_DIST_DIR, currentTarget);
unlinkSync(ELECTRON_APP_PATH);
if (existsSync(oldTargetPath)) {
renameSync(oldTargetPath, desiredAppPath);
}
symlinkSync(DESIRED_APP_NAME, ELECTRON_APP_PATH);
actualAppPath = desiredAppPath;
} else {
// Different workspace name from previous run — update
const oldTargetPath = resolve(ELECTRON_DIST_DIR, currentTarget);
unlinkSync(ELECTRON_APP_PATH);
if (existsSync(oldTargetPath)) {
renameSync(oldTargetPath, desiredAppPath);
} else if (!existsSync(desiredAppPath)) {
console.warn("[patch-dev-protocol] Previous Electron.app not found and desired target missing — re-install electron");
process.exit(1);
}
symlinkSync(DESIRED_APP_NAME, ELECTRON_APP_PATH);
actualAppPath = desiredAppPath;
🤖 Prompt for AI Agents
In `@apps/desktop/scripts/patch-dev-protocol.ts` around lines 186 - 194, The code
can create a dangling symlink when oldTargetPath is missing; modify the block
around unlinkSync/renameSync/symlinkSync so you only create the symlink when the
target directory exists (or create the directory first). Specifically, in the
else branch handling a workspace-name change, after computing oldTargetPath and
before calling symlinkSync(ELECTRON_APP_PATH,...), check
existsSync(desiredAppPath) (or call mkdirSync(desiredAppPath, { recursive: true
}) to ensure it exists) and only then call symlinkSync(DESIRED_APP_NAME,
ELECTRON_APP_PATH) and set actualAppPath = desiredAppPath; otherwise skip
creating the symlink (or fail early) to avoid leaving a dangling link.

}
} else {
// Real directory — rename and create symlink
if (existsSync(desiredAppPath)) {
rmSync(desiredAppPath, { recursive: true });
}
renameSync(ELECTRON_APP_PATH, desiredAppPath);
symlinkSync(DESIRED_APP_NAME, ELECTRON_APP_PATH);
actualAppPath = desiredAppPath;
}

console.log(
`[patch-dev-protocol] Renamed Electron.app to ${DESIRED_APP_NAME}`,
);
} catch (err) {
console.warn("[patch-dev-protocol] Failed to rename Electron.app:", err);
}

try {
execSync(
`/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -f "${ELECTRON_APP_PATH}"`,
`/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -f "${actualAppPath}"`,
);
console.log(
`[patch-dev-protocol] Registered ${PROTOCOL_SCHEME}:// with Launch Services`,
Expand All @@ -125,3 +223,18 @@ try {
err,
);
}

// Update the electron package's path.txt so electron-vite launches from the
// renamed .app directly (not through the Electron.app symlink). This ensures
// the invocation path contains the correct app name for macOS bundle resolution.
const electronPkgDir = resolve(import.meta.dirname, "../node_modules/electron");
const pathTxtPath = resolve(electronPkgDir, "path.txt");
const desiredPathTxt = `${DESIRED_APP_NAME}/Contents/MacOS/Electron`;
try {
writeFileSync(pathTxtPath, desiredPathTxt);
console.log(
`[patch-dev-protocol] Updated path.txt to use ${DESIRED_APP_NAME}`,
);
} catch (err) {
console.warn("[patch-dev-protocol] Failed to update path.txt:", err);
}
11 changes: 8 additions & 3 deletions apps/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { getWorkspaceName } from "shared/env.shared";
import { setupAgentHooks } from "./lib/agent-setup";
import { initAppState } from "./lib/app-state";
import { setupAutoUpdater } from "./lib/auto-updater";
import { setWorkspaceDockIcon } from "./lib/dock-icon";
import { localDb } from "./lib/local-db";
import { ensureProjectIconsDir, getProjectIconPath } from "./lib/project-icons";
import { initSentry } from "./lib/sentry";
Expand All @@ -21,9 +22,12 @@ import { MainWindow } from "./windows/main";

console.log("[main] Local database ready:", !!localDb);

const workspaceName = getWorkspaceName();
if (workspaceName) {
app.setName(`Superset (${workspaceName})`);
// Dev mode: label the app with the workspace name so multiple worktrees are distinguishable
if (process.env.NODE_ENV === "development") {
const workspaceName = getWorkspaceName();
if (workspaceName) {
app.setName(`Superset (${workspaceName})`);
}
}

// Dev mode: register with execPath + app script so macOS launches Electron with our entry point
Expand Down Expand Up @@ -230,6 +234,7 @@ if (!gotTheLock) {
.protocol.handle("superset-icon", iconProtocolHandler);

ensureProjectIconsDir();
setWorkspaceDockIcon();
initSentry();
await initAppState();

Expand Down
Loading
Loading