Skip to content
Open
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
9 changes: 7 additions & 2 deletions apps/desktop/electron-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,10 @@ const config: Configuration = {
"!**/.DS_Store",
],

// Rebuild native modules for Electron's Node.js version
npmRebuild: true,
// Rebuild native modules for Electron's Node.js version.
// Disabled on Windows — native modules are already handled by install:deps
// and copy:native-modules, and node-gyp may fail without Visual Studio.
npmRebuild: process.platform !== "win32",

// macOS
mac: {
Expand Down Expand Up @@ -204,6 +206,9 @@ const config: Configuration = {
nsis: {
oneClick: false,
allowToChangeInstallationDirectory: true,
createDesktopShortcut: true,
createStartMenuShortcut: true,
shortcutName: productName,
},
};

Expand Down
7 changes: 7 additions & 0 deletions apps/desktop/electron.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
defineEnv,
devPath,
htmlEnvTransformPlugin,
stripCrossOriginPlugin,
} from "./vite/helpers";

// override: true ensures .env values take precedence over inherited env vars
Expand Down Expand Up @@ -104,6 +105,11 @@ export default defineConfig({
},
output: {
dir: resolve(devPath, "main"),
// VS Code and other Electron hosts set ELECTRON_RUN_AS_NODE=1 which
// prevents Electron from entering browser mode. Clear it before any
// require("electron") call — must be the very first statement.
banner:
'delete process.env.ELECTRON_RUN_AS_NODE;',
},
external: [
"electron",
Expand Down Expand Up @@ -235,6 +241,7 @@ export default defineConfig({
}),
reactPlugin(),
htmlEnvTransformPlugin(),
stripCrossOriginPlugin(),
],

worker: {
Expand Down
9 changes: 7 additions & 2 deletions apps/desktop/scripts/copy-native-modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,13 @@ function copyModuleIfSymlink(
console.log(` ${moduleName}: symlink -> replacing with real files`);
console.log(` Real path: ${realPath}`);

// Remove the symlink
rmSync(modulePath);
// Windows uses junctions (directory-like) instead of symlinks;
// rmSync needs { recursive: true } to remove them.
if (process.platform === "win32") {
rmSync(modulePath, { recursive: true, force: true });
} else {
rmSync(modulePath);
}

// Copy the actual files
cpSync(realPath, modulePath, { recursive: true });
Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/src/lib/electron-app/factories/app/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ export async function makeAppSetup(
return window;
}

PLATFORM.IS_LINUX && app.disableHardwareAcceleration();
// Disable GPU hardware acceleration on Linux and Windows to prevent black/blank
// screens caused by GPU driver incompatibilities with Chromium's compositor.
(PLATFORM.IS_LINUX || PLATFORM.IS_WINDOWS) && app.disableHardwareAcceleration();

// macOS Sequoia+: occluded window throttling can corrupt GPU compositor layers
if (PLATFORM.IS_MAC) {
Expand Down
8 changes: 7 additions & 1 deletion apps/desktop/src/lib/window-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,14 @@ export function registerRoute(props: {
const url = `http://localhost:${env.DESKTOP_VITE_PORT}/#/`;
console.log("[window-loader] Loading development URL:", url);
props.browserWindow.loadURL(url);
} else if (process.platform === "win32") {
// Production (Windows): use custom protocol for proper dynamic import support.
// file:// protocol breaks ES module dynamic imports (code-split chunks) on Windows.
const url = "superset-app://app/index.html#/";
console.log("[window-loader] Loading custom protocol URL:", url);
props.browserWindow.loadURL(url);
} else {
// Production: load from file with hash routing
// Production (macOS/Linux): load from file with hash routing
// TanStack Router uses hash-based routing, so we always start at #/
console.log("[window-loader] Loading file:", props.htmlFile);
props.browserWindow.loadFile(props.htmlFile, { hash: "/" });
Expand Down
51 changes: 51 additions & 0 deletions apps/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,15 @@ protocol.registerSchemesAsPrivileged([
supportFetchAPI: true,
},
},
{
scheme: "superset-app",
privileges: {
standard: true,
secure: true,
supportFetchAPI: true,
corsEnabled: true,
},
},
]);

const gotTheLock = app.requestSingleInstanceLock();
Expand Down Expand Up @@ -283,6 +292,48 @@ if (!gotTheLock) {
.fromPartition("persist:superset")
.protocol.handle("superset-icon", iconProtocolHandler);

// Register custom protocol for serving renderer files.
// Dynamic imports (code-split chunks) fail on file:// protocol in Electron on Windows.
// The superset-app:// protocol serves files from the renderer dist directory with
// proper CORS and module support, enabling lazy-loaded route components.
const rendererDir = path.join(__dirname, "../renderer");
const appProtocolHandler = (request: Request) => {
let urlPath = new URL(request.url).pathname;
// Remove leading slash on Windows
if (urlPath.startsWith("/")) urlPath = urlPath.slice(1);
const filePath = path.join(rendererDir, urlPath);
return net.fetch(pathToFileURL(filePath).toString());
Comment on lines +304 to +305
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 6, 2026

Choose a reason for hiding this comment

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

P1: Sanitize superset-app:// paths before joining to rendererDir; as written, ../ traversal can escape the renderer directory and expose arbitrary local files via the custom protocol.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/main/index.ts, line 304:

<comment>Sanitize `superset-app://` paths before joining to `rendererDir`; as written, `../` traversal can escape the renderer directory and expose arbitrary local files via the custom protocol.</comment>

<file context>
@@ -283,6 +292,48 @@ if (!gotTheLock) {
+			let urlPath = new URL(request.url).pathname;
+			// Remove leading slash on Windows
+			if (urlPath.startsWith("/")) urlPath = urlPath.slice(1);
+			const filePath = path.join(rendererDir, urlPath);
+			return net.fetch(pathToFileURL(filePath).toString());
+		};
</file context>
Suggested change
const filePath = path.join(rendererDir, urlPath);
return net.fetch(pathToFileURL(filePath).toString());
const resolvedPath = path.resolve(rendererDir, urlPath);
const relativePath = path.relative(rendererDir, resolvedPath);
if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
return new Response("Not found", { status: 404 });
}
return net.fetch(pathToFileURL(resolvedPath).toString());
Fix with Cubic

};
Comment on lines +299 to +306
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 | 🔴 Critical

Path traversal vulnerability in protocol handler.

The urlPath extracted from the request URL is joined directly to rendererDir without validation. A malicious request like superset-app://app/../../../etc/passwd could escape the renderer directory and serve arbitrary files.

🛡️ Proposed fix: Validate resolved path stays within rendererDir
 		const appProtocolHandler = (request: Request) => {
 			let urlPath = new URL(request.url).pathname;
 			// Remove leading slash on Windows
 			if (urlPath.startsWith("/")) urlPath = urlPath.slice(1);
+			// Decode URL-encoded characters and normalize
+			urlPath = decodeURIComponent(urlPath);
 			const filePath = path.join(rendererDir, urlPath);
+			// Prevent path traversal attacks
+			const resolvedPath = path.resolve(filePath);
+			if (!resolvedPath.startsWith(path.resolve(rendererDir) + path.sep) && resolvedPath !== path.resolve(rendererDir)) {
+				return new Response("Forbidden", { status: 403 });
+			}
 			return net.fetch(pathToFileURL(filePath).toString());
 		};
📝 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
const rendererDir = path.join(__dirname, "../renderer");
const appProtocolHandler = (request: Request) => {
let urlPath = new URL(request.url).pathname;
// Remove leading slash on Windows
if (urlPath.startsWith("/")) urlPath = urlPath.slice(1);
const filePath = path.join(rendererDir, urlPath);
return net.fetch(pathToFileURL(filePath).toString());
};
const rendererDir = path.join(__dirname, "../renderer");
const appProtocolHandler = (request: Request) => {
let urlPath = new URL(request.url).pathname;
// Remove leading slash on Windows
if (urlPath.startsWith("/")) urlPath = urlPath.slice(1);
// Decode URL-encoded characters and normalize
urlPath = decodeURIComponent(urlPath);
const filePath = path.join(rendererDir, urlPath);
// Prevent path traversal attacks
const resolvedPath = path.resolve(filePath);
if (!resolvedPath.startsWith(path.resolve(rendererDir) + path.sep) && resolvedPath !== path.resolve(rendererDir)) {
return new Response("Forbidden", { status: 403 });
}
return net.fetch(pathToFileURL(filePath).toString());
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/index.ts` around lines 299 - 306, The protocol handler
(appProtocolHandler) currently joins the unvalidated urlPath into rendererDir
allowing path traversal (e.g., ../../../). Fix by resolving the requested path
(use path.resolve) and verifying the resolved path is inside rendererDir (e.g.,
compare resolved.startsWith(rendererDir) or ensure path.relative(rendererDir,
resolved) doesn’t start with '..'); if the check fails, return a safe error
response instead of calling net.fetch on the out-of-bounds filePath. Keep the
remaining flow (pathToFileURL and net.fetch) but only use them after the
directory containment check.

protocol.handle("superset-app", appProtocolHandler);
session
.fromPartition("persist:superset")
.protocol.handle("superset-app", appProtocolHandler);

// On Windows, the custom superset-app:// protocol origin is not recognized by
// the API server's CORS policy. Bypass CORS for API requests by modifying headers.
if (PLATFORM.IS_WINDOWS) {
const appSession = session.fromPartition("persist:superset");
appSession.webRequest.onBeforeSendHeaders(
{ urls: ["https://api.superset.sh/*", "https://*.posthog.com/*", "https://*.sentry.io/*", "https://app.outlit.ai/*"] },
(details, callback) => {
// Replace custom protocol origin with the expected web origin
if (details.requestHeaders.Origin === "superset-app://app") {
delete details.requestHeaders.Origin;
}
callback({ requestHeaders: details.requestHeaders });
},
);
appSession.webRequest.onHeadersReceived(
{ urls: ["https://api.superset.sh/*"] },
(details, callback) => {
const headers = details.responseHeaders ?? {};
headers["access-control-allow-origin"] = ["superset-app://app"];
headers["access-control-allow-credentials"] = ["true"];
callback({ responseHeaders: headers });
},
);
Comment on lines +316 to +334
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 | 🟠 Major

Hardcoded URLs in CORS bypass may drift from environment configuration.

The URL patterns are hardcoded, but apps/desktop/src/main/env.main.ts defines these URLs via environment variables (NEXT_PUBLIC_API_URL, NEXT_PUBLIC_STREAMS_URL, NEXT_PUBLIC_ELECTRIC_URL, NEXT_PUBLIC_POSTHOG_HOST). This creates a maintenance risk where env changes won't be reflected in the CORS bypass.

Additionally:

  • onBeforeSendHeaders covers 4 URL patterns, but onHeadersReceived only covers api.superset.sh/*
  • Missing streams.superset.sh and electric-proxy URLs that are also configured in env
♻️ Suggested approach: Build URL patterns from env
 		if (PLATFORM.IS_WINDOWS) {
+			// Import env from ./env.main or access via shared config
+			const corsUrls = [
+				`${env.NEXT_PUBLIC_API_URL}/*`,
+				`${env.NEXT_PUBLIC_STREAMS_URL}/*`,
+				`${env.NEXT_PUBLIC_POSTHOG_HOST}/*`,
+				"https://*.sentry.io/*",
+				"https://app.outlit.ai/*",
+			];
 			const appSession = session.fromPartition("persist:superset");
 			appSession.webRequest.onBeforeSendHeaders(
-				{ urls: ["https://api.superset.sh/*", "https://*.posthog.com/*", "https://*.sentry.io/*", "https://app.outlit.ai/*"] },
+				{ urls: corsUrls },
 				(details, callback) => {

Also consider applying onHeadersReceived to the same URL set for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/index.ts` around lines 316 - 334, The CORS bypass uses
hardcoded URL strings; update appSession.webRequest.onBeforeSendHeaders and
appSession.webRequest.onHeadersReceived to build the URL pattern list from the
environment vars defined in env.main.ts (NEXT_PUBLIC_API_URL,
NEXT_PUBLIC_STREAMS_URL, NEXT_PUBLIC_ELECTRIC_URL, NEXT_PUBLIC_POSTHOG_HOST)
instead of literal "api.superset.sh", "streams.superset.sh", "electric-proxy",
and posthog URLs; ensure the constructed array includes streams and
electric-proxy entries and use the same array for both handlers (fall back to
sensible defaults or skip entries if an env var is missing) so
onBeforeSendHeaders and onHeadersReceived remain consistent.

}

ensureProjectIconsDir();
setWorkspaceDockIcon();
initSentry();
Expand Down
16 changes: 16 additions & 0 deletions apps/desktop/src/main/windows/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,22 @@ export async function MainWindow() {
});
}

// Forward renderer warning/error messages to main process stdout for Windows debugging.
if (PLATFORM.IS_WINDOWS) {
window.webContents.on(
"console-message",
(_event, level, message, line, sourceId) => {
if (level < 2) return;
const levelStr =
["verbose", "info", "warning", "error"][level] ?? "unknown";
const source = sourceId ? ` (${sourceId}:${line})` : "";
const formatted = `[renderer:${levelStr}] ${message}${source}`;
if (level === 3) console.error(formatted);
else console.warn(formatted);
},
);
}

// Persist window bounds on move/resize so state survives app.exit(0)
// (which skips the close handler — e.g. electron-vite SIGTERM during dev).
// Gated by `initialized` so the initial maximize() doesn't immediately
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ export function CollectionsProvider({ children }: { children: ReactNode }) {
preloadActiveOrganizationCollections(activeOrganizationId);
}, [activeOrganizationId]);

if (useElectricCloud === undefined) {
return null;
}
// When PostHog is not configured (no key), feature flags stay undefined forever.
// Default to false (use proxy) so the app doesn't block rendering.
const isElectricCloud = useElectricCloud ?? false;
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 6, 2026

Choose a reason for hiding this comment

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

P2: Defaulting useElectricCloud to false causes collections to be created with the proxy URL while feature flags are still loading. Since collections are cached and capture electricUrl at creation, they won’t switch to the cloud URL when the flag later resolves to true, so the app can stay stuck on the proxy backend.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.tsx, line 69:

<comment>Defaulting `useElectricCloud` to false causes collections to be created with the proxy URL while feature flags are still loading. Since collections are cached and capture `electricUrl` at creation, they won’t switch to the cloud URL when the flag later resolves to true, so the app can stay stuck on the proxy backend.</comment>

<file context>
@@ -64,12 +64,12 @@ export function CollectionsProvider({ children }: { children: ReactNode }) {
-	}
+	// When PostHog is not configured (no key), feature flags stay undefined forever.
+	// Default to false (use proxy) so the app doesn't block rendering.
+	const isElectricCloud = useElectricCloud ?? false;
 
 	setElectricUrl(
</file context>
Suggested change
const isElectricCloud = useElectricCloud ?? false;
const isElectricCloud =
useElectricCloud ?? (env.NEXT_PUBLIC_POSTHOG_KEY ? undefined : false);
if (isElectricCloud === undefined) {
return null;
}
Fix with Cubic


setElectricUrl(
useElectricCloud
isElectricCloud
? env.NEXT_PUBLIC_ELECTRIC_URL
: env.NEXT_PUBLIC_ELECTRIC_PROXY_URL,
);
Comment on lines +67 to 75
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE='apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts'

echo '--- collections.ts (first 260 lines) ---'
head -260 "$FILE" | nl -ba

echo
echo '--- electricUrl state + collection cache references ---'
rg -n -C3 'electricUrl|setElectricUrl|getCollections|preloadCollections|Map|cache' "$FILE"

Repository: superset-sh/superset

Length of output: 164


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE='apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts'

echo '--- collections.ts (first 260 lines with line numbers) ---'
head -260 "$FILE" | cat -n

echo
echo '--- electricUrl state + collection cache references ---'
rg -n -C3 'electricUrl|setElectricUrl|getCollections|preloadCollections|Map|cache' "$FILE" || true

Repository: superset-sh/superset

Length of output: 15682


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE='apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.tsx'

echo '--- CollectionsProvider.tsx (lines 40-80) ---'
sed -n '40,80p' "$FILE" | cat -n -v

echo
echo '--- useEffect blocks and related logic ---'
rg -n -B2 -A5 'useEffect|useElectricCloud|setElectricUrl|preloadActiveOrganizationCollections' "$FILE"

Repository: superset-sh/superset

Length of output: 3509


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- Searching for FEATURE_FLAGS.ELECTRIC_CLOUD definition ---'
rg -n 'FEATURE_FLAGS|ELECTRIC_CLOUD' apps/desktop/src/renderer --type ts --type tsx -B2 -A2 | head -50

echo
echo '--- Checking PostHog hook behavior in imports ---'
rg -n 'useFeatureFlagEnabled' apps/desktop/src/renderer --type ts --type tsx | head -20

Repository: superset-sh/superset

Length of output: 170


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- Searching for FEATURE_FLAGS.ELECTRIC_CLOUD definition ---'
rg -n 'FEATURE_FLAGS|ELECTRIC_CLOUD' apps/desktop/src/renderer -g '*.ts' -g '*.tsx' -B2 -A2 | head -60

echo
echo '--- Checking env.renderer for PostHog key ---'
cat apps/desktop/src/renderer/env.renderer.ts | cat -n

Repository: superset-sh/superset

Length of output: 10539


Don't treat a loading flag the same as a disabled flag.

Line 69 collapses both "PostHog is not configured" and "the flag has not resolved yet" into false. On a PostHog-enabled startup, that means lines 71-75 point electricUrl at the proxy before the real flag arrives. Meanwhile, lines 63-65 preload collections only once per activeOrganizationId. If the flag later resolves to true, the preload effect never re-triggers because activeOrganizationId hasn't changed. Since collections.ts caches collections per org with the electricUrl value baked in at creation time, cloud-enabled users can stay on the proxy for that session.

Suggested fix
-	// When PostHog is not configured (no key), feature flags stay undefined forever.
-	// Default to false (use proxy) so the app doesn't block rendering.
-	const isElectricCloud = useElectricCloud ?? false;
-
-	setElectricUrl(
-		isElectricCloud
-			? env.NEXT_PUBLIC_ELECTRIC_URL
-			: env.NEXT_PUBLIC_ELECTRIC_PROXY_URL,
-	);
+	const isPostHogConfigured = Boolean(env.NEXT_PUBLIC_POSTHOG_KEY);
+	const isElectricCloud = isPostHogConfigured ? useElectricCloud : false;
+	const electricBaseUrl =
+		isElectricCloud === true
+			? env.NEXT_PUBLIC_ELECTRIC_URL
+			: env.NEXT_PUBLIC_ELECTRIC_PROXY_URL;
+
+	useEffect(() => {
+		setElectricUrl(electricBaseUrl);
+	}, [electricBaseUrl]);
 
-	useEffect(() => {
-		preloadActiveOrganizationCollections(activeOrganizationId);
-	}, [activeOrganizationId]);
+	useEffect(() => {
+		if (isPostHogConfigured && isElectricCloud === undefined) return;
+		preloadActiveOrganizationCollections(activeOrganizationId);
+	}, [activeOrganizationId, electricBaseUrl, isPostHogConfigured, isElectricCloud]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.tsx`
around lines 67 - 75, The code currently collapses an unresolved feature flag
into false by using `const isElectricCloud = useElectricCloud ?? false`, causing
`setElectricUrl` (in the `CollectionsProvider`) to point at the proxy before the
flag resolves and preventing the preload effect (which depends only on
`activeOrganizationId`) from re-running; change the logic so `useElectricCloud`
is treated tri-state (true/false/undefined) — only set the `electricUrl` to the
proxy when `useElectricCloud === false`, set it to cloud URL when
`useElectricCloud === true`, and do not change `electricUrl` while
`useElectricCloud` is `undefined`; additionally, ensure the preload effect that
fetches collections (the effect referencing `activeOrganizationId`) also depends
on the resolved flag (either `useElectricCloud` or derived `electricUrl`) so it
re-triggers when the feature flag resolves and avoids using the cached
proxy-bound collections from `collections.ts`.

Expand Down
20 changes: 19 additions & 1 deletion apps/desktop/vite/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function defineEnv(
value: string | undefined,
fallback?: string,
): string {
return JSON.stringify(value ?? fallback);
return JSON.stringify(value || fallback);
}

const RESOURCES_TO_COPY = [
Expand Down Expand Up @@ -62,6 +62,24 @@ export function copyResourcesPlugin(): Plugin {
};
}

/**
* Strips the `crossorigin` attribute that Vite adds to script/link tags.
* Electron's ASAR file:// handler doesn't support CORS on Windows,
* so crossorigin causes scripts/styles to silently fail to load (black screen).
*/
export function stripCrossOriginPlugin(): Plugin {
return {
name: "strip-crossorigin",
transformIndexHtml: {
order: "post",
handler(html) {
if (process.platform !== "win32") return html;
return html.replace(/ crossorigin(?:="[^"]*")?/g, "");
},
},
};
}

/**
* Injects environment variables into index.html CSP.
*/
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"format:check": "biome format .",
"typecheck": "turbo typecheck",
"ui-add": "turbo run ui-add",
"postinstall": "./scripts/postinstall.sh",
"postinstall": "node scripts/postinstall.mjs",
"clean": "git clean -xdf node_modules",
"clean:workspaces": "turbo clean",
"release:desktop": "./apps/desktop/create-release.sh",
Expand Down
48 changes: 48 additions & 0 deletions scripts/postinstall.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Cross-platform postinstall script.
*
* Replaces the bash-only postinstall.sh so that `bun install` works on
* Windows, macOS and Linux without special flags.
*
* Steps:
* 1. Guard against infinite recursion (electron-builder install-app-deps
* can trigger nested bun installs which would re-run this script).
* 2. Run sherif for workspace validation.
* 3. Install native dependencies for the desktop app.
*/

import { execSync } from "node:child_process";

// Prevent infinite recursion during postinstall
if (process.env.SUPERSET_POSTINSTALL_RUNNING) {
process.exit(0);
}
process.env.SUPERSET_POSTINSTALL_RUNNING = "1";

const env = { ...process.env, SUPERSET_POSTINSTALL_RUNNING: "1" };

/** Run a command, inheriting stdio so output is visible. */
function run(cmd) {
execSync(cmd, { stdio: "inherit", env });
}

/** Run a command but don't fail if it errors (for optional native deps on Windows). */
function tryRun(cmd, label) {
try {
execSync(cmd, { stdio: "inherit", env });
} catch {
console.warn(`[postinstall] ${label} failed (non-fatal on Windows) — continuing`);
}
}

// Run sherif for workspace validation
run("sherif");

// Install native dependencies for desktop app.
// On Windows, native module compilation may fail if Visual Studio Build Tools
// are not installed. This is non-fatal — prebuilt binaries will be used when available.
if (process.platform === "win32") {
tryRun("bun run --filter=@superset/desktop install:deps", "install:deps");
} else {
run("bun run --filter=@superset/desktop install:deps");
}
Comment on lines +29 to +48
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 | 🟠 Major

Don't swallow every Windows install:deps failure.

This makes bun install succeed even when the desktop native runtime is only partially prepared, and the first hard failure then moves downstream into apps/desktop/scripts/copy-native-modules.ts / apps/desktop/scripts/validate-native-runtime.ts. If you want to tolerate missing Build Tools, only downgrade known node-gyp cases and then assert that the required runtime packages are actually present.

Suggested guard
 import { execSync } from "node:child_process";
+import { existsSync } from "node:fs";
+import { join } from "node:path";
@@
 function tryRun(cmd, label) {
 	try {
 		execSync(cmd, { stdio: "inherit", env });
 	} catch {
 		console.warn(`[postinstall] ${label} failed (non-fatal on Windows) — continuing`);
 	}
 }
+
+function assertDesktopNativeDepsInstalled() {
+	const required = [
+		"better-sqlite3",
+		"node-pty",
+		"@ast-grep/napi",
+		"libsql",
+	];
+	const missing = required.filter(
+		(name) =>
+			!existsSync(join("apps/desktop/node_modules", name, "package.json")),
+	);
+	if (missing.length) {
+		throw new Error(
+			`[postinstall] Missing desktop native deps after install:deps: ${missing.join(", ")}`,
+		);
+	}
+}
@@
 if (process.platform === "win32") {
 	tryRun("bun run --filter=@superset/desktop install:deps", "install:deps");
+	assertDesktopNativeDepsInstalled();
 } else {
 	run("bun run --filter=@superset/desktop install:deps");
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/postinstall.mjs` around lines 29 - 48, The current tryRun wrapper
swallows all errors from "bun run --filter=@superset/desktop install:deps" on
Windows; change it so only known, tolerated node-gyp/build-tools errors are
ignored and all other failures are rethrown. Update tryRun (and the Windows
branch that calls it) to capture the thrown error, match its message/exit code
against an allowlist of node-gyp/Visual Studio Build Tools patterns, and only
log-and-continue for those matches; otherwise rethrow the error so bun install
fails fast. Additionally, after a tolerated failure, invoke the desktop runtime
validation (the existing validate-native-runtime/copy-native-modules check or
the script apps/desktop/scripts/validate-native-runtime.ts) to assert required
runtime packages are present and fail if they are missing.