diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml
index b32ea6a59fd..ea0a150a0df 100644
--- a/.github/workflows/build-desktop.yml
+++ b/.github/workflows/build-desktop.yml
@@ -261,3 +261,139 @@ jobs:
path: apps/desktop/release/*-linux.yml
retention-days: ${{ inputs.artifact_retention_days }}
if-no-files-found: error
+
+ build-windows:
+ name: Build - Windows (x64)
+ runs-on: windows-latest
+ environment: production
+ # Applies to Git spawned by actions/checkout (Git 2.31+). Shell-only config is too late for checkout.
+ env:
+ GIT_CONFIG_COUNT: 1
+ GIT_CONFIG_KEY_0: core.longpaths
+ GIT_CONFIG_VALUE_0: "true"
+
+ steps:
+ # OS + global config (redundant with env above; helps older Git / edge cases).
+ - name: Enable Windows long paths
+ shell: powershell
+ run: |
+ git config --global core.longpaths true
+ if (-not (Test-Path 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem')) { exit 0 }
+ try {
+ Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value 1 -Type DWord -Force -ErrorAction Stop
+ Write-Host 'LongPathsEnabled=1 set'
+ } catch {
+ Write-Host 'Registry LongPathsEnabled skipped (no permission):' $_.Exception.Message
+ }
+
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup Bun
+ id: setup-bun
+ uses: oven-sh/setup-bun@v2
+ with:
+ bun-version-file: .bun-version
+
+ - name: Cache dependencies
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.bun/install/cache
+ key: ${{ runner.os }}-bun-${{ steps.setup-bun.outputs.bun-revision }}-${{ github.sha }}
+ restore-keys: |
+ ${{ runner.os }}-bun-${{ steps.setup-bun.outputs.bun-revision }}-
+
+ - name: Install dependencies
+ run: bun install --frozen --ignore-scripts
+
+ - name: Install desktop native dependencies
+ working-directory: apps/desktop
+ run: bun run install:deps
+
+ - name: Set version suffix
+ if: inputs.version_suffix != ''
+ working-directory: apps/desktop
+ shell: bash
+ run: |
+ CURRENT_VERSION=$(node -p "require('./package.json').version")
+ NEW_VERSION="${CURRENT_VERSION}${{ inputs.version_suffix }}"
+ echo "Setting version to: $NEW_VERSION"
+ node -e "
+ const fs = require('fs');
+ const pkg = require('./package.json');
+ pkg.version = '$NEW_VERSION';
+ fs.writeFileSync('./package.json', JSON.stringify(pkg, null, '\t') + '\n');
+ "
+ echo "Updated package.json version to $NEW_VERSION"
+
+ - name: Clean dev folder
+ working-directory: apps/desktop
+ run: bun run clean:dev
+
+ - name: Generate file icons
+ working-directory: apps/desktop
+ run: bun run generate:icons
+
+ - name: Compile app with electron-vite
+ working-directory: apps/desktop
+ env:
+ NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }}
+ NEXT_PUBLIC_POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }}
+ GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
+ GH_CLIENT_ID: ${{ secrets.GH_CLIENT_ID }}
+ NEXT_PUBLIC_WEB_URL: ${{ secrets.NEXT_PUBLIC_WEB_URL }}
+ NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}
+ NEXT_PUBLIC_DOCS_URL: ${{ secrets.NEXT_PUBLIC_DOCS_URL }}
+ NEXT_PUBLIC_STREAMS_URL: ${{ secrets.NEXT_PUBLIC_STREAMS_URL }}
+ NEXT_PUBLIC_ELECTRIC_URL: ${{ secrets.NEXT_PUBLIC_ELECTRIC_URL }}
+ SENTRY_DSN_DESKTOP: ${{ secrets.SENTRY_DSN_DESKTOP }}
+ SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
+ SUPERSET_WORKSPACE_NAME: superset
+ run: bun run compile:app
+
+ - name: Build Electron app
+ working-directory: apps/desktop
+ env:
+ CSC_IDENTITY_AUTO_DISCOVERY: "false"
+ run: bun run package -- --publish never --config ${{ inputs.electron_builder_config }}
+
+ - name: Verify Windows NSIS + update manifest exist
+ working-directory: apps/desktop
+ shell: bash
+ run: |
+ ls -la release
+ test -n "$(ls -1 release/*-x64.exe 2>/dev/null | grep -v __uninstaller || true)" || {
+ echo "::error::No NSIS installer (*.exe) generated in apps/desktop/release"
+ exit 1
+ }
+ test -f release/latest.yml || {
+ echo "::error::latest.yml missing from apps/desktop/release — auto-update may be broken"
+ exit 1
+ }
+
+ - name: Upload Windows NSIS installer
+ uses: actions/upload-artifact@v4
+ with:
+ name: ${{ inputs.artifact_prefix }}-win-x64-nsis
+ path: |
+ apps/desktop/release/*-x64.exe
+ !apps/desktop/release/**/*__uninstaller*.exe
+ retention-days: ${{ inputs.artifact_retention_days }}
+ if-no-files-found: error
+
+ - name: Upload Windows block map
+ uses: actions/upload-artifact@v4
+ with:
+ name: ${{ inputs.artifact_prefix }}-win-x64-blockmap
+ path: apps/desktop/release/*.blockmap
+ retention-days: ${{ inputs.artifact_retention_days }}
+ if-no-files-found: warn
+
+ - name: Upload Windows auto-update manifest
+ uses: actions/upload-artifact@v4
+ with:
+ name: ${{ inputs.artifact_prefix }}-win-x64-update-manifest
+ path: apps/desktop/release/latest.yml
+ retention-days: ${{ inputs.artifact_retention_days }}
+ if-no-files-found: error
diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml
index 717b1a3806d..d98a248a48a 100644
--- a/.github/workflows/release-desktop.yml
+++ b/.github/workflows/release-desktop.yml
@@ -107,6 +107,14 @@ jobs:
break
fi
done
+ # Windows NSIS: stable name for /releases/latest/download/Superset-x64.exe
+ for file in *-x64.exe; do
+ if [[ -f "$file" && "$file" != *"__uninstaller"* ]]; then
+ cp "$file" "Superset-x64.exe"
+ echo "Created stable copy: Superset-x64.exe"
+ break
+ fi
+ done
echo "Release artifacts:"
ls -la
diff --git a/README.md b/README.md
index 86ef7998d8a..5d4cc67c663 100644
--- a/README.md
+++ b/README.md
@@ -4,8 +4,10 @@
### The Code Editor for AI Agents
-[](https://github.com/superset-sh/superset/stargazers)
-[](https://github.com/superset-sh/superset/releases)
+**Fork [quueli/superset-windows](https://github.com/quueli/superset-windows)** — Windows 10+ desktop builds, cross-platform `postinstall`, and Windows CI workflows. Upstream: [superset-sh/superset](https://github.com/superset-sh/superset).
+
+[](https://github.com/quueli/superset-windows/stargazers)
+[](https://github.com/quueli/superset-windows/releases)
[](LICENSE.md)
[](https://x.com/superset_sh)
[](https://discord.gg/cZeD9WYcV7)
@@ -17,7 +19,7 @@ Works with any CLI agent. Built for local worktree-based development.
-[**Download for macOS**](https://github.com/superset-sh/superset/releases/latest) • [Documentation](https://docs.superset.sh) • [Changelog](https://github.com/superset-sh/superset/releases) • [Discord](https://discord.gg/cZeD9WYcV7)
+[**Windows installer (x64)**](https://github.com/quueli/superset-windows/releases/latest) • [**macOS (upstream)**](https://github.com/superset-sh/superset/releases/latest) • [Documentation](https://docs.superset.sh) • [Changelog (upstream)](https://github.com/superset-sh/superset/releases) • [Discord](https://discord.gg/cZeD9WYcV7)
@@ -71,7 +73,7 @@ If it runs in a terminal, it runs on Superset
| Requirement | Details |
|:------------|:--------|
-| **OS** | macOS (Windows/Linux untested) |
+| **OS** | **Windows 10+ x64** (builds from this fork). macOS / Linux — same as upstream. On Windows, run `git config --global core.longpaths true` **before** cloning to avoid long-path errors. |
| **Runtime** | [Bun](https://bun.sh/) v1.0+ |
| **Version Control** | Git 2.20+ |
| **GitHub CLI** | [gh](https://cli.github.com/) |
@@ -81,7 +83,8 @@ If it runs in a terminal, it runs on Superset
### Quick Start (Pre-built)
-**[Download Superset for macOS](https://github.com/superset-sh/superset/releases/latest)**
+- **Windows x64:** [последний релиз форка](https://github.com/quueli/superset-windows/releases/latest) — установщик NSIS (`Superset-*-x64.exe` или стабильное имя `Superset-x64.exe` в релизе).
+- **macOS:** [официальные сборки upstream](https://github.com/superset-sh/superset/releases/latest).
### Build from Source
@@ -91,10 +94,14 @@ If it runs in a terminal, it runs on Superset
**1. Clone the repository**
```bash
-git clone https://github.com/superset-sh/superset.git
-cd superset
+git clone https://github.com/quueli/superset-windows.git
+cd superset-windows
```
+На **Windows** перед клоном (при ошибке «Filename too long»): `git config --global core.longpaths true`.
+
+Для нативных модулей десктопа на Windows нужны **Visual Studio Build Tools** с рабочей нагрузкой **Desktop development with C++** (MSVC + Windows SDK).
+
**2. Set up environment variables** (choose one):
Option A: Full setup
@@ -127,11 +134,62 @@ bun run dev
```bash
bun run build
-open apps/desktop/release
+```
+
+Артефакты: `apps/desktop/release/` — на Windows установщик **NSIS** `*-x64.exe`, на macOS `.dmg` / `.zip`, на Linux `.AppImage`.
+
+Локально только Windows-инсталлятор:
+
+```bash
+cd apps/desktop
+bun run clean:dev && bun run generate:icons && bun run compile:app
+set CSC_IDENTITY_AUTO_DISCOVERY=false # cmd
+# PowerShell: $env:CSC_IDENTITY_AUTO_DISCOVERY='false'
+bun run package
```
+## Releases in this fork / Релизы в форке
+
+GitHub Actions собирает **macOS**, **Linux** и **Windows (x64)**. Финальный **GitHub Release** с файлами создаётся **только при push тега** вида `desktop-v*.*.*` (например `desktop-v1.4.8`). Job `release` не запускается от обычного push в ветку.
+
+### Вариант A — релиз через тег (рекомендуется)
+
+1. Убедитесь, что секреты для environment **`production`** в репозитории заданы (как у upstream: переменные для `compile:app`, при необходимости Sentry и т.д.). Без них шаг компиляции в CI может упасть.
+2. Обновите версию в [`apps/desktop/package.json`](apps/desktop/package.json) (`version`), закоммитьте.
+3. Создайте и отправьте тег:
+
+```bash
+git checkout main
+git pull origin main
+git tag desktop-v1.4.8
+git push origin desktop-v1.4.8
+```
+
+4. Откройте **Actions** → workflow **Release Desktop App** → дождитесь окончания job **build** (все платформы) и **release**.
+5. В репозитории появится **черновик** релиза (draft). Проверьте вложения (`.dmg`, `.AppImage`, `.exe`, манифесты), затем нажмите **Publish release**.
+
+Стабильные имена для прямых ссылок (скрипт релиза создаёт копии): например `Superset-x64.exe`, `Superset-arm64.dmg`, `latest-linux.yml`.
+
+### Вариант B — только сборка без автоматического релиза
+
+В **Actions** запустите **Release Desktop App** вручную (**Run workflow**). Соберутся артефакты для скачивания из вкладки run, но шаг **Create GitHub Release** выполняется только при условии `refs/tags/desktop-v*` — для полноценного релиза всё равно используйте тег из варианта A.
+
+### Вариант C — локальная сборка и ручная загрузка
+
+Соберите `apps/desktop/release/*` локально (см. выше), затем в GitHub: **Releases** → **Draft a new release** → прикрепите файлы и опубликуйте.
+
+### Синхронизация с upstream
+
+```bash
+git remote add upstream https://github.com/superset-sh/superset.git # один раз
+git fetch upstream
+git checkout main
+git merge upstream/main
+# разрешите конфликты, проверьте сборку, затем push в origin
+```
+
## Keyboard Shortcuts
All shortcuts are customizable via **Settings > Keyboard Shortcuts** (`⌘/`). See [full documentation](https://docs.superset.sh/keyboard-shortcuts).
@@ -230,17 +288,17 @@ This repo uses the published upstream `mastracode` and `@mastra/*` packages dire
## Contributing
-We welcome contributions! If you have a suggestion that would make Superset better:
+Contributions to **this fork** (Windows, CI, документация):
-1. Fork the repository
-2. Create your feature branch (`git checkout -b feature/amazing-feature`)
-3. Commit your changes (`git commit -m 'Add amazing feature'`)
-4. Push to the branch (`git push origin feature/amazing-feature`)
-5. Open a Pull Request
+1. Клонируйте [quueli/superset-windows](https://github.com/quueli/superset-windows) (или свой форк от него).
+2. Ветка фичи: `git checkout -b feature/amazing-feature`
+3. Коммит: `git commit -m 'Add amazing feature'`
+4. Пуш: `git push origin feature/amazing-feature`
+5. Откройте **Pull Request в `quueli/superset-windows`**.
-You can also [open issues](https://github.com/superset-sh/superset/issues) for bugs or feature requests.
+Issues и обсуждения по **апстриму**: [superset-sh/superset/issues](https://github.com/superset-sh/superset/issues). По сборке Windows и релизам форка удобнее заводить issue в [quueli/superset-windows/issues](https://github.com/quueli/superset-windows/issues).
-See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed instructions and code of conduct.
+Общие правила проекта: [CONTRIBUTING.md](CONTRIBUTING.md) (ориентир — upstream).
@@ -252,7 +310,8 @@ Join the Superset community to get help, share feedback, and connect with other
- **[Discord](https://discord.gg/cZeD9WYcV7)** — Chat with the team and community
- **[Twitter](https://x.com/superset_sh)** — Follow for updates and announcements
-- **[GitHub Issues](https://github.com/superset-sh/superset/issues)** — Report bugs and request features
+- **[GitHub Issues (upstream)](https://github.com/superset-sh/superset/issues)** — Report bugs and request features
+- **[Issues (this fork)](https://github.com/quueli/superset-windows/issues)** — Windows build / fork-specific
- **[GitHub Discussions](https://github.com/superset-sh/superset/discussions)** — Ask questions and share ideas
### Team
diff --git a/apps/desktop/package.json b/apps/desktop/package.json
index 93471c4e10d..391a56694c7 100644
--- a/apps/desktop/package.json
+++ b/apps/desktop/package.json
@@ -2,7 +2,7 @@
"name": "@superset/desktop",
"productName": "Superset",
"description": "The last developer tool you'll ever need",
- "version": "1.4.7",
+ "version": "1.4.8",
"main": "./dist/main/index.js",
"resources": "src/resources",
"repository": {
@@ -26,6 +26,8 @@
"build": "cross-env CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --publish never",
"prepackage": "bun run copy:native-modules && bun run validate:native-runtime",
"package": "electron-builder --config electron-builder.ts",
+ "patch:node-pty-win-spectre": "bun ../../scripts/patch-node-pty-spectre-windows.ts",
+ "preinstall:deps": "bun run patch:node-pty-win-spectre",
"install:deps": "electron-builder install-app-deps",
"release": "electron-builder --publish always",
"clean:dev": "rimraf ./node_modules/.dev",
diff --git a/apps/desktop/scripts/copy-native-modules.ts b/apps/desktop/scripts/copy-native-modules.ts
index 3b34f3ecc75..ed24a0b49ee 100644
--- a/apps/desktop/scripts/copy-native-modules.ts
+++ b/apps/desktop/scripts/copy-native-modules.ts
@@ -13,7 +13,7 @@
* This is safe because bun install will recreate the symlinks on next install.
*/
-import { execSync } from "node:child_process";
+import { execSync, spawnSync } from "node:child_process";
import {
cpSync,
existsSync,
@@ -23,6 +23,7 @@ import {
readFileSync,
realpathSync,
rmSync,
+ unlinkSync,
} from "node:fs";
import { dirname, join } from "node:path";
import { satisfies } from "semver";
@@ -34,10 +35,64 @@ import { requiredMaterializedNodeModules } from "../runtime-dependencies";
const TARGET_ARCH = process.env.TARGET_ARCH || process.arch;
const TARGET_PLATFORM = process.env.TARGET_PLATFORM || process.platform;
+/**
+ * Deep copy of a directory tree. On Windows, Bun's cpSync can hit EPERM when the
+ * tree contains nested symlinks (e.g. workspace packages); robocopy follows files reliably.
+ */
+function copyDirectoryTree(src: string, dest: string): void {
+ if (process.platform === "win32") {
+ mkdirSync(dest, { recursive: true });
+ const st = spawnSync(
+ "robocopy",
+ [
+ src,
+ dest,
+ "/E",
+ "/COPY:DAT",
+ "/R:2",
+ "/W:1",
+ "/NFL",
+ "/NDL",
+ "/NJH",
+ "/NJS",
+ ],
+ { stdio: "pipe", encoding: "utf8", windowsHide: true },
+ );
+ const code = st.status ?? 0;
+ if (code >= 8) {
+ throw new Error(
+ `robocopy failed (${code}): ${st.stderr ?? st.stdout ?? ""}`,
+ );
+ }
+ return;
+ }
+ cpSync(src, dest, { recursive: true });
+}
+
function getWorkspaceRootNodeModulesDir(nodeModulesDir: string): string {
return join(nodeModulesDir, "..", "..", "..", "node_modules");
}
+/** Repo root: apps/desktop/node_modules -> ../../../ */
+function getMonorepoRootFromDesktopNodeModules(nodeModulesDir: string): string {
+ return join(nodeModulesDir, "..", "..", "..");
+}
+
+/** Workspace `packages/` for `@superset/` (e.g. macos-process-metrics). */
+function resolveWorkspaceSupersetPackageDir(
+ nodeModulesDir: string,
+ moduleName: string,
+): string | null {
+ if (!moduleName.startsWith("@superset/")) return null;
+ const shortName = moduleName.slice("@superset/".length);
+ const candidate = join(
+ getMonorepoRootFromDesktopNodeModules(nodeModulesDir),
+ "packages",
+ shortName,
+ );
+ return existsSync(join(candidate, "package.json")) ? candidate : null;
+}
+
function getBunFlatNodeModulesDir(nodeModulesDir: string): string {
return join(
getWorkspaceRootNodeModulesDir(nodeModulesDir),
@@ -77,7 +132,7 @@ function copyModuleIfSymlink(
if (existsSync(bunFlatModulePath)) {
console.log(` ${moduleName}: materializing from Bun store index`);
mkdirSync(dirname(modulePath), { recursive: true });
- cpSync(realpathSync(bunFlatModulePath), modulePath, { recursive: true });
+ copyDirectoryTree(realpathSync(bunFlatModulePath), modulePath);
console.log(` Copied to: ${modulePath}`);
return true;
}
@@ -97,13 +152,43 @@ function copyModuleIfSymlink(
console.log(` ${moduleName}: symlink -> replacing with real files`);
console.log(` Real path: ${realPath}`);
- // Remove the symlink
- rmSync(modulePath);
+ // Remove the symlink/junction (unlink avoids Bun/Windows EFAULT from rmSync on links)
+ try {
+ unlinkSync(modulePath);
+ } catch {
+ rmSync(modulePath, { recursive: true, maxRetries: 3, force: true });
+ }
// Copy the actual files
- cpSync(realPath, modulePath, { recursive: true });
+ copyDirectoryTree(realPath, modulePath);
console.log(` Copied to: ${modulePath}`);
+ } else if (!existsSync(join(modulePath, "package.json"))) {
+ // Leftover empty dir (e.g. failed copy) — materialize from workspace or Bun store
+ const workspaceSrc = resolveWorkspaceSupersetPackageDir(
+ nodeModulesDir,
+ moduleName,
+ );
+ if (workspaceSrc) {
+ console.log(
+ ` ${moduleName}: repairing broken/empty install from workspace`,
+ );
+ rmSync(modulePath, { recursive: true, maxRetries: 3, force: true });
+ copyDirectoryTree(workspaceSrc, modulePath);
+ console.log(` Copied to: ${modulePath}`);
+ } else if (existsSync(bunFlatModulePath)) {
+ console.log(
+ ` ${moduleName}: repairing broken/empty install from Bun store`,
+ );
+ rmSync(modulePath, { recursive: true, maxRetries: 3, force: true });
+ copyDirectoryTree(realpathSync(bunFlatModulePath), modulePath);
+ console.log(` Copied to: ${modulePath}`);
+ } else if (required) {
+ console.error(
+ ` [ERROR] ${moduleName} at ${modulePath} has no package.json and no repair source`,
+ );
+ process.exit(1);
+ }
} else {
console.log(` ${moduleName}: already real directory (not a symlink)`);
}
@@ -143,7 +228,7 @@ function copyExactModuleVersion(
);
if (existsSync(sourcePath)) {
mkdirSync(dirname(destPath), { recursive: true });
- cpSync(sourcePath, destPath, { recursive: true });
+ copyDirectoryTree(sourcePath, destPath);
console.log(` Copied ${moduleName}@${version} to: ${destPath}`);
return true;
}
@@ -203,10 +288,16 @@ function copyDependencyForPackage(
const nestedStats = lstatSync(nestedDependencyPath);
if (nestedStats.isSymbolicLink()) {
const realPath = realpathSync(nestedDependencyPath);
- rmSync(nestedDependencyPath);
- cpSync(realPath, nestedDependencyPath, {
- recursive: true,
- });
+ try {
+ unlinkSync(nestedDependencyPath);
+ } catch {
+ rmSync(nestedDependencyPath, {
+ recursive: true,
+ maxRetries: 3,
+ force: true,
+ });
+ }
+ copyDirectoryTree(realPath, nestedDependencyPath);
}
return;
}
@@ -315,7 +406,7 @@ function copyAstGrepPlatformPackages(nodeModulesDir: string): void {
if (existsSync(sourcePath)) {
console.log(` ${platformPkg.name}: copying from Bun store`);
mkdirSync(dirname(destPath), { recursive: true });
- cpSync(sourcePath, destPath, { recursive: true });
+ copyDirectoryTree(sourcePath, destPath);
if (isTargetPkg) resolvedTargetPackage = true;
continue;
}
@@ -458,7 +549,7 @@ function copyParcelWatcherPlatformPackages(nodeModulesDir: string): void {
console.log(` ${platformPkg.name}: copying from Bun store`);
mkdirSync(dirname(destPath), { recursive: true });
- cpSync(sourcePath, destPath, { recursive: true });
+ copyDirectoryTree(sourcePath, destPath);
resolvedPlatformPackage = true;
}
diff --git a/package.json b/package.json
index 2cef5488f9f..d4d19cbefbc 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,8 @@
"name": "@superset/repo",
"version": "0.0.0",
"repository": {
- "type": "git"
+ "type": "git",
+ "url": "https://github.com/superset-sh/superset.git"
},
"devDependencies": {
"@biomejs/biome": "2.4.2",
@@ -30,7 +31,7 @@
"format:check": "bunx @biomejs/biome@2.4.2 format .",
"typecheck": "turbo typecheck",
"ui-add": "turbo run ui-add",
- "postinstall": "./scripts/postinstall.sh",
+ "postinstall": "bun scripts/postinstall.ts",
"clean": "git clean -xdf node_modules",
"clean:workspaces": "turbo clean",
"release:desktop": "./apps/desktop/create-release.sh",
diff --git a/scripts/patch-node-pty-spectre-windows.ts b/scripts/patch-node-pty-spectre-windows.ts
new file mode 100644
index 00000000000..1dd820020e6
--- /dev/null
+++ b/scripts/patch-node-pty-spectre-windows.ts
@@ -0,0 +1,43 @@
+/**
+ * node-pty enables MSVC Spectre mitigation in its gyp files, which requires
+ * optional "Spectre-mitigated libs" in VS Build Tools. Many Windows dev setups
+ * omit those; disabling mitigation matches relaxed local builds and fixes MSB8040.
+ */
+import { readFileSync, writeFileSync } from "node:fs";
+import { createRequire } from "node:module";
+import { dirname, join } from "node:path";
+
+export function patchNodePtySpectreForWindows(): void {
+ if (process.platform !== "win32") {
+ return;
+ }
+
+ let nodePtyDir: string;
+ try {
+ const req = createRequire(join(process.cwd(), "apps/desktop/package.json"));
+ const pkgJson = req.resolve("node-pty/package.json");
+ nodePtyDir = dirname(pkgJson);
+ } catch {
+ return;
+ }
+
+ const files = [
+ join(nodePtyDir, "binding.gyp"),
+ join(nodePtyDir, "deps", "winpty", "src", "winpty.gyp"),
+ ];
+
+ for (const file of files) {
+ try {
+ const before = readFileSync(file, "utf8");
+ const after = before.replaceAll(
+ "'SpectreMitigation': 'Spectre'",
+ "'SpectreMitigation': 'false'",
+ );
+ if (after !== before) {
+ writeFileSync(file, after);
+ }
+ } catch {
+ // Optional paths (e.g. layout change) — ignore.
+ }
+ }
+}
diff --git a/scripts/postinstall.ts b/scripts/postinstall.ts
new file mode 100644
index 00000000000..4196fdd1f9b
--- /dev/null
+++ b/scripts/postinstall.ts
@@ -0,0 +1,38 @@
+/**
+ * Cross-platform postinstall (replaces postinstall.sh for Windows).
+ * Keeps the same behavior: sherif validation, then desktop native deps (non-CI).
+ */
+import { spawnSync } from "node:child_process";
+import { patchNodePtySpectreForWindows } from "./patch-node-pty-spectre-windows.ts";
+
+if (process.env.SUPERSET_POSTINSTALL_RUNNING) {
+ process.exit(0);
+}
+process.env.SUPERSET_POSTINSTALL_RUNNING = "1";
+
+function run(cmd: string, args: string[]): number {
+ const r = spawnSync(cmd, args, {
+ stdio: "inherit",
+ shell: process.platform === "win32",
+ env: process.env,
+ });
+ return r.status ?? (r.error ? 1 : 0);
+}
+
+const sherifExit = run("bunx", ["sherif"]);
+if (sherifExit !== 0) {
+ process.exit(sherifExit);
+}
+
+if (process.env.CI) {
+ process.exit(0);
+}
+
+patchNodePtySpectreForWindows();
+
+const depsExit = run("bun", [
+ "run",
+ "--filter=@superset/desktop",
+ "install:deps",
+]);
+process.exit(depsExit);