diff --git a/.env.example b/.env.example index dda41bee262..862e3b59a7b 100644 --- a/.env.example +++ b/.env.example @@ -1,14 +1,2 @@ -# Database connection URL for local PostgreSQL -# Connect to the 'superset' database DATABASE_URL=postgresql://postgres:postgres@localhost:5432/superset - -STUB_API_KEY= - -# Electron Desktop App -# Vite dev server port for Electron renderer process -# Default: 4927. Auto-increments when creating new worktrees to avoid port conflicts VITE_DEV_SERVER_PORT=4927 - -# Enable new UI layout (workspace tabs at top, worktree sidebar) -# Set to 'true' to enable the new mock UI for visual iteration -ENABLE_NEW_UI=false diff --git a/DISTRIBUTION_PLAN.md b/DISTRIBUTION_PLAN.md deleted file mode 100644 index 044b0963e68..00000000000 --- a/DISTRIBUTION_PLAN.md +++ /dev/null @@ -1,669 +0,0 @@ -# Desktop App Distribution Plan - -## Current State - -### What We Have -- **electron-builder** configured (`electron-builder.ts:1`) -- **Build scripts** in place (`apps/desktop/package.json:20-23`) - - `bun run build` - Local build - - `bun run release` - Publish build -- **Platform targets** configured: - - **macOS**: ZIP, DMG, DIR - - **Linux**: AppImage, DEB, Pacman, FreeBSD, RPM - - **Windows**: ZIP, Portable -- **Icons** ready (`src/resources/build/icons/`) -- **electron-vite** for app compilation -- **Basic CI** (lint, typecheck, build) - -### What's Missing -- Auto-update infrastructure -- Code signing & notarization -- Release automation via CI/CD -- Version management strategy -- Distribution hosting -- Update server/CDN -- Security considerations - ---- - -## Distribution Strategy - -### Phase 1: Manual Releases (Quickest Path to Distribution) - -#### 1.1 Improve electron-builder Configuration - -**File**: `apps/desktop/electron-builder.ts` - -**Add to configuration:** -```typescript -export default { - // ... existing config - - // Compression & artifacts - compression: "maximum", - - // File associations (optional) - fileAssociations: [ - { - ext: "superset", - name: "Superset Workspace", - role: "Editor" - } - ], - - // macOS specific - mac: { - // ... existing - hardenedRuntime: true, - gatekeeperAssess: false, - entitlements: "build/entitlements.mac.plist", - entitlementsInherit: "build/entitlements.mac.plist", - notarize: false // Enable later with credentials - }, - - // macOS DMG - dmg: { - contents: [ - { - x: 130, - y: 220 - }, - { - x: 410, - y: 220, - type: "link", - path: "/Applications" - } - ] - }, - - // Windows specific - win: { - // ... existing - target: [ - { - target: "nsis", - arch: ["x64", "arm64"] - }, - "zip", - "portable" - ] - }, - - // NSIS installer options - nsis: { - oneClick: false, - allowToChangeInstallationDirectory: true, - createDesktopShortcut: true, - createStartMenuShortcut: true, - shortcutName: displayName - }, - - // Linux AppImage - appImage: { - license: "LICENSE" - } -} satisfies Configuration; -``` - -**Action items:** -- Create `apps/desktop/build/entitlements.mac.plist` for macOS entitlements -- Add more granular build targets as needed -- Consider adding Windows signing stub (for later) - -#### 1.2 Version Management - -**Strategy**: Use semantic versioning with manual bumps - -**Add to root `package.json` scripts:** -```json -{ - "scripts": { - "version:patch": "bun run version:bump patch", - "version:minor": "bun run version:bump minor", - "version:major": "bun run version:bump major", - "version:bump": "tsx scripts/bump-version.ts" - } -} -``` - -**Create**: `scripts/bump-version.ts` -- Bump version in `apps/desktop/package.json` -- Update changelog -- Create git tag - -#### 1.3 Manual Release Process - -**Steps:** -1. `bun run version:patch` (or minor/major) -2. Commit version bump -3. `cd apps/desktop && bun run prebuild` -4. `bun run build` (or per-platform: `bun run build --mac`, `--win`, `--linux`) -5. Test installers locally -6. Upload to GitHub Releases manually -7. Push git tag - -**Pros:** -- Simple, no infrastructure needed -- Can start immediately -- Full control over each release - -**Cons:** -- Manual work -- Prone to human error -- No automated testing of installers - ---- - -### Phase 2: GitHub Releases + Auto-Updates - -#### 2.1 GitHub Releases via CI/CD - -**Create**: `.github/workflows/release-desktop.yml` - -```yaml -name: Release Desktop App - -on: - push: - tags: - - 'desktop-v*' - -jobs: - release: - strategy: - matrix: - os: [macos-latest, ubuntu-latest, windows-latest] - - runs-on: ${{ matrix.os }} - - steps: - - uses: actions/checkout@v4 - - - uses: oven-sh/setup-bun@v1 - with: - bun-version: latest - - - name: Install dependencies - run: bun install --frozen - - - name: Build desktop app - run: | - cd apps/desktop - bun run prebuild - bun run build - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.os }}-build - path: apps/desktop/dist/**/* - - - name: Release - uses: softprops/action-gh-release@v1 - with: - files: apps/desktop/dist/**/* - draft: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -``` - -**Trigger release:** -```bash -git tag desktop-v0.1.0 -git push origin desktop-v0.1.0 -``` - -#### 2.2 Auto-Update Configuration - -**Add to `electron-builder.ts`:** -```typescript -export default { - // ... existing - - publish: { - provider: "github", - owner: "your-org", - repo: "superset", - releaseType: "release" // or "draft" - } -} satisfies Configuration; -``` - -**Install auto-updater:** -```bash -cd apps/desktop -bun add electron-updater -``` - -**Create**: `apps/desktop/src/main/lib/auto-updater.ts` - -```typescript -import { autoUpdater } from "electron-updater"; -import { app, dialog } from "electron"; - -export function initAutoUpdater() { - // Disable auto-download - autoUpdater.autoDownload = false; - - // Check for updates on startup (after 3 seconds) - setTimeout(() => { - autoUpdater.checkForUpdates(); - }, 3000); - - // When update is available - autoUpdater.on("update-available", (info) => { - dialog.showMessageBox({ - type: "info", - title: "Update Available", - message: `Version ${info.version} is available. Do you want to download it now?`, - buttons: ["Download", "Later"] - }).then((result) => { - if (result.response === 0) { - autoUpdater.downloadUpdate(); - } - }); - }); - - // Download progress - autoUpdater.on("download-progress", (progress) => { - // Send to renderer process to show progress bar - // mainWindow.webContents.send("download-progress", progress.percent); - }); - - // Update downloaded - autoUpdater.on("update-downloaded", () => { - dialog.showMessageBox({ - type: "info", - title: "Update Ready", - message: "Update downloaded. Restart the app to apply the update?", - buttons: ["Restart", "Later"] - }).then((result) => { - if (result.response === 0) { - autoUpdater.quitAndInstall(); - } - }); - }); -} -``` - -**Import in main process** (`src/main/index.ts`): -```typescript -import { initAutoUpdater } from "./lib/auto-updater"; - -app.whenReady().then(() => { - // ... existing setup - - if (!app.isPackaged) { - // Skip auto-updater in development - return; - } - - initAutoUpdater(); -}); -``` - -#### 2.3 Update Server Options - -**Option A: GitHub Releases** (Free) -- Simple, no infrastructure -- Uses `electron-updater` + `provider: "github"` -- Rate limits may apply - -**Option B: Custom CDN** -- Full control -- Better performance -- Requires setup (S3 + CloudFront, etc.) - -**Option C: Electron Release Server** -- Self-hosted -- More features (channels, statistics) -- Requires maintenance - -**Recommended**: Start with GitHub Releases (Option A) - ---- - -### Phase 3: Code Signing & Notarization - -#### 3.1 macOS Code Signing - -**Requirements:** -- Apple Developer account ($99/year) -- Developer ID Application certificate -- Developer ID Installer certificate - -**Setup:** -1. Create certificates in Apple Developer portal -2. Download and install in Keychain -3. Set environment variables in CI: - - `CSC_LINK` (base64-encoded .p12 file) - - `CSC_KEY_PASSWORD` (certificate password) - - `APPLE_ID` (for notarization) - - `APPLE_APP_SPECIFIC_PASSWORD` (app-specific password) - - `APPLE_TEAM_ID` - -**Update CI workflow:** -```yaml -- name: Build desktop app (macOS) - if: matrix.os == 'macos-latest' - run: | - cd apps/desktop - bun run prebuild - bun run build - env: - CSC_LINK: ${{ secrets.MAC_CERT_P12_BASE64 }} - CSC_KEY_PASSWORD: ${{ secrets.MAC_CERT_PASSWORD }} - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} -``` - -**Update `electron-builder.ts`:** -```typescript -mac: { - // ... existing - hardenedRuntime: true, - gatekeeperAssess: false, - notarize: { - teamId: process.env.APPLE_TEAM_ID - } -} -``` - -#### 3.2 Windows Code Signing - -**Requirements:** -- Code signing certificate (from DigiCert, Sectigo, etc.) -- Certificate file (.pfx or .p12) - -**Setup:** -1. Purchase certificate -2. Set environment variables in CI: - - `WIN_CSC_LINK` (base64-encoded .pfx file) - - `WIN_CSC_KEY_PASSWORD` - -**Update CI workflow:** -```yaml -- name: Build desktop app (Windows) - if: matrix.os == 'windows-latest' - run: | - cd apps/desktop - bun run prebuild - bun run build - env: - WIN_CSC_LINK: ${{ secrets.WIN_CERT_P12_BASE64 }} - WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CERT_PASSWORD }} -``` - -#### 3.3 Linux Signing - -**Not typically required**, but can sign AppImage: -- Use GPG signature -- Or sign with private key - ---- - -### Phase 4: Advanced Distribution - -#### 4.1 Release Channels - -**Implement channels**: `stable`, `beta`, `alpha` - -**Strategy:** -- `stable`: Production releases (git tags: `v1.0.0`) -- `beta`: Pre-release testing (git tags: `v1.0.0-beta.1`) -- `alpha`: Nightly/development (git tags: `v1.0.0-alpha.1`) - -**Update electron-builder.ts:** -```typescript -publish: [ - { - provider: "github", - owner: "your-org", - repo: "superset", - channel: process.env.RELEASE_CHANNEL || "stable" - } -] -``` - -**Allow users to switch channels** in app settings. - -#### 4.2 Crash Reporting - -**Options:** -- Sentry -- BugSnag -- Electron's built-in crash reporter - -**Example with Sentry:** -```bash -bun add @sentry/electron -``` - -```typescript -// src/main/index.ts -import * as Sentry from "@sentry/electron/main"; - -Sentry.init({ - dsn: "YOUR_SENTRY_DSN" -}); -``` - -#### 4.3 Analytics - -**Options:** -- Posthog (privacy-friendly) -- Mixpanel -- Custom solution - -**Privacy considerations:** -- Make analytics opt-in -- Anonymize data -- Respect Do Not Track - -#### 4.4 App Store Distribution - -**macOS App Store:** -- Requires sandboxing -- More restrictive entitlements -- May conflict with git/terminal features (node-pty) -- **Recommendation**: Skip unless necessary - -**Microsoft Store:** -- Requires different packaging (MSIX) -- Add to `electron-builder.ts`: - ```typescript - win: { - target: ["nsis", "appx"] - }, - appx: { - displayName: "Superset", - publisherDisplayName: "Your Name", - identityName: "YourCompany.Superset" - } - ``` - -**Linux stores:** -- Snap Store -- Flathub -- Requires separate packaging - ---- - -## Security Considerations - -### 1. Dependency Security -- Run `bun audit` regularly -- Keep electron and dependencies updated -- Use Dependabot for automated updates - -### 2. Content Security Policy -Already likely in place, but ensure CSP headers are set in renderer process. - -### 3. Native Module Security -- `node-pty` requires native compilation -- Ensure it's rebuilt for correct Electron version -- Test on all platforms after updates - -### 4. Update Security -- Use HTTPS for update server -- Verify signatures of updates -- electron-updater handles this by default with code-signed apps - -### 5. Secrets Management -- Never commit certificates or keys -- Use GitHub Secrets for CI/CD -- Rotate credentials regularly - ---- - -## Testing Strategy - -### 1. Pre-Release Testing -**Create checklist:** -- [ ] Install on clean macOS system -- [ ] Install on clean Windows system -- [ ] Install on clean Linux system (Ubuntu, Fedora) -- [ ] Test auto-updater (from previous version) -- [ ] Verify code signature (macOS: `codesign -vvv -d`, Windows: right-click properties) -- [ ] Test all major features (terminal, workspace, etc.) -- [ ] Check for console errors -- [ ] Verify permissions (file access, etc.) - -### 2. Beta Testing Program -- Recruit beta testers -- Use `beta` release channel -- Collect feedback via Discord/GitHub -- Monitor crash reports - -### 3. Automated Testing -**Add to CI:** -```yaml -- name: Test app launch (smoke test) - run: | - cd apps/desktop - xvfb-run --auto-servernum bun run start & - sleep 5 - pkill -f electron -``` - ---- - -## Rollout Plan - -### Week 1: Foundation -- [ ] Enhance `electron-builder.ts` with improved config -- [ ] Create version bump script -- [ ] Create entitlements file for macOS -- [ ] Test local builds on all platforms -- [ ] Document release process - -### Week 2: Automation -- [ ] Create `.github/workflows/release-desktop.yml` -- [ ] Set up GitHub Releases -- [ ] Test CI build on all platforms -- [ ] Add auto-updater code -- [ ] Test update flow locally - -### Week 3: Code Signing -- [ ] Purchase/set up certificates (macOS, Windows) -- [ ] Configure CI secrets -- [ ] Add signing to CI workflow -- [ ] Test signed builds -- [ ] Verify notarization (macOS) - -### Week 4: Launch -- [ ] Create v0.1.0 release -- [ ] Publish release notes -- [ ] Announce on website/social media -- [ ] Monitor for issues -- [ ] Collect feedback - -### Ongoing -- [ ] Set up crash reporting -- [ ] Add analytics (opt-in) -- [ ] Implement beta channel -- [ ] Improve update UX -- [ ] App store distribution (if needed) - ---- - -## Costs - -| Item | Cost | Notes | -|------|------|-------| -| Apple Developer Account | $99/year | Required for macOS notarization | -| Windows Code Signing | $100-400/year | From DigiCert, Sectigo, etc. | -| Crash Reporting (Sentry) | $0-29+/month | Free tier available | -| CDN (if not using GitHub) | $0-50+/month | AWS S3 + CloudFront | -| **Total (Year 1)** | **~$200-600** | Minimum: $199 (Apple + Win cert) | - ---- - -## Resources - -### Documentation -- [electron-builder docs](https://www.electron.build/) -- [electron-updater docs](https://www.electron.build/auto-update) -- [Apple notarization guide](https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution) -- [Windows code signing](https://docs.microsoft.com/en-us/windows/win32/seccrypto/cryptography-tools) - -### Tools -- [Electron Forge](https://www.electronforge.io/) (alternative to electron-builder) -- [Electron Release Server](https://github.com/ArekSredzki/electron-release-server) -- [update-electron-app](https://github.com/electron/update-electron-app) (simpler updater) - -### Community -- [Electron Discord](https://discord.gg/electron) -- [electron-builder issues](https://github.com/electron-userland/electron-builder/issues) - ---- - -## Recommended Starting Point - -**For fastest time-to-distribution:** - -1. **Enhance config** (1-2 hours) - - Update `electron-builder.ts` with NSIS, DMG settings - - Create entitlements file - -2. **Manual release** (1 hour) - - Build locally: `cd apps/desktop && bun run prebuild && bun run build` - - Test installers - - Upload to GitHub Releases - -3. **Get feedback** (1 week) - - Share with early adopters - - Collect bug reports - - Iterate - -4. **Automate** (1-2 days) - - Set up CI workflow - - Add auto-updater - - Test end-to-end - -5. **Sign** (2-3 days) - - Get certificates - - Configure signing - - Test signed builds - -**Total estimated time: 1-2 weeks** for a production-ready distribution setup. - ---- - -## Questions to Answer - -Before proceeding, clarify: - -1. **Target audience**: Who will use this? (developers, teams, enterprises) -2. **Platform priority**: macOS-first? Windows? Linux? -3. **Update frequency**: Weekly? Monthly? Ad-hoc? -4. **Budget**: Can you afford code signing certificates? -5. **Privacy**: Will you collect analytics? Crash reports? -6. **Distribution model**: Free? Paid? Freemium? -7. **Support model**: Community? Email? Discord? - -These decisions will affect the implementation details. diff --git a/apps/desktop/.npmrc b/apps/desktop/.npmrc index 92177f47f01..e69de29bb2d 100644 --- a/apps/desktop/.npmrc +++ b/apps/desktop/.npmrc @@ -1,3 +0,0 @@ -auto-install-peers=true -shamefully-hoist=true -strict-peer-dependencies=false diff --git a/apps/desktop/CLAUDE.md b/apps/desktop/CLAUDE.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/desktop/electron-builder.ts b/apps/desktop/electron-builder.ts index ea108d93390..d1e2077e56f 100644 --- a/apps/desktop/electron-builder.ts +++ b/apps/desktop/electron-builder.ts @@ -1,62 +1,90 @@ -/** biome-ignore-all lint/suspicious/noTemplateCurlyInString: <> */ +/** + * Electron Builder Configuration + * @see https://www.electron.build/configuration/configuration + */ -import { dirname } from "node:path"; +import { join } from "node:path"; import type { Configuration } from "electron-builder"; +import pkg from "./package.json"; -import { - author as _author, - description, - displayName, - main, - name, - resources, - version, -} from "./package.json"; - -const author = _author?.name ?? _author; const currentYear = new Date().getFullYear(); +const author = pkg.author?.name ?? pkg.author; const authorInKebabCase = author.replace(/\s+/g, "-"); -const appId = `com.${authorInKebabCase}.${name}`.toLowerCase(); +const appId = `com.${authorInKebabCase}.${pkg.name}`.toLowerCase(); -const artifactName = [`${name}-v${version}`, "-${os}.${ext}"].join(""); - -export default { +const config: Configuration = { appId, - productName: displayName, + productName: pkg.displayName, copyright: `Copyright © ${currentYear} — ${author}`, + electronVersion: pkg.devDependencies.electron.replace(/^\^/, ""), + // Directories directories: { - app: dirname(main), - output: `dist/v${version}`, + output: "release", + buildResources: join(pkg.resources, "build"), }, + files: [ + "dist/**/*", + "package.json", + { + from: pkg.resources, + to: "resources", + filter: ["**/*"], + }, + "!node_modules/@superset/**/*", + ], + + // Build optimization npmRebuild: false, buildDependenciesFromSource: false, nodeGypRebuild: false, + // macOS mac: { - artifactName, - icon: `${resources}/build/icons/icon.icns`, + icon: join(pkg.resources, "build/icons/icon.icns"), category: "public.app-category.utilities", - target: ["zip", "dmg", "dir"], + target: [ + { + target: "default", + arch: ["universal"], + }, + ], + hardenedRuntime: true, + gatekeeperAssess: false, notarize: false, }, + // Deep linking protocol protocols: { - name: displayName, + name: pkg.displayName, schemes: ["superset"], }, + // Linux linux: { - artifactName, - category: "Utilities", - synopsis: description, - target: ["AppImage", "deb", "pacman", "freebsd", "rpm"], + icon: join(pkg.resources, "build/icons"), + category: "Utility", + synopsis: pkg.description, + target: ["AppImage", "deb"], }, + // Windows win: { - artifactName, - icon: `${resources}/build/icons/icon.ico`, - target: ["zip", "portable"], + icon: join(pkg.resources, "build/icons/icon.ico"), + target: [ + { + target: "nsis", + arch: ["x64"], + }, + ], }, -} satisfies Configuration; + + // NSIS installer (Windows) + nsis: { + oneClick: false, + allowToChangeInstallationDirectory: true, + }, +}; + +export default config; diff --git a/apps/desktop/electron.vite.config.ts b/apps/desktop/electron.vite.config.ts index dd26b79825d..b2091b74c86 100644 --- a/apps/desktop/electron.vite.config.ts +++ b/apps/desktop/electron.vite.config.ts @@ -25,7 +25,7 @@ export default defineConfig({ plugins: [ tsconfigPaths, externalizeDepsPlugin({ - exclude: ["@superset/cli"], + exclude: ["@superset/*"], }), ], @@ -36,14 +36,12 @@ export default defineConfig({ }, output: { - dir: resolve(devPath, "main"), + dir: resolve(devPath), }, }, }, resolve: { - alias: { - "@superset/cli": resolve(__dirname, "../../apps/cli/src"), - }, + alias: {}, }, }, diff --git a/apps/desktop/package.json b/apps/desktop/package.json index a60ac8ef626..4c3d45a8d9b 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,15 +1,14 @@ { "displayName": "Superset", - "name": "Superset", + "name": "@superset/desktop", "description": "The last developer tool you'll ever need", "version": "0.0.0", - "main": "./node_modules/.dev/main/index.js", + "main": "./dist/main/index.js", "resources": "src/resources", "author": { - "name": "Dalton Menezes", - "email": "daltonmenezes@outlook.com" + "name": "Superset", + "email": "hi@superset.com" }, - "license": "MIT", "scripts": { "start": "electron-vite preview", "predev": "bun run clean:dev", @@ -33,9 +32,6 @@ "@dnd-kit/utilities": "^3.2.2", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.8", - "@superset/api": "workspace:*", - "@superset/cli": "workspace:*", - "@superset/ui": "workspace:*", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-search": "^0.15.0", "@xterm/addon-web-links": "^0.11.0", @@ -63,6 +59,7 @@ }, "devDependencies": { "@biomejs/biome": "^2.2.6", + "@superset/ui": "workspace:*", "@tailwindcss/vite": "^4.0.9", "@types/http-proxy": "^1.17.17", "@types/node": "^24.9.1", diff --git a/apps/desktop/src/lib/electron-app/factories/app/instance.ts b/apps/desktop/src/lib/electron-app/factories/app/instance.ts index 5c3f0d1bae0..e925089967b 100644 --- a/apps/desktop/src/lib/electron-app/factories/app/instance.ts +++ b/apps/desktop/src/lib/electron-app/factories/app/instance.ts @@ -2,6 +2,5 @@ import { app } from "electron"; export function makeAppWithSingleInstanceLock(fn: () => void) { const isPrimaryInstance = app.requestSingleInstanceLock(); - !isPrimaryInstance ? app.quit() : fn(); } diff --git a/apps/desktop/src/lib/electron-app/factories/app/setup.ts b/apps/desktop/src/lib/electron-app/factories/app/setup.ts index b8fdb0321a6..4fc50ee23b8 100644 --- a/apps/desktop/src/lib/electron-app/factories/app/setup.ts +++ b/apps/desktop/src/lib/electron-app/factories/app/setup.ts @@ -4,7 +4,6 @@ import { installExtension, REACT_DEVELOPER_TOOLS, } from "electron-extension-installer"; -import terminalManager from "main/lib/terminal"; import { ENVIRONMENT, PLATFORM } from "shared/constants"; import { makeAppId } from "shared/utils"; import { ignoreConsoleWarnings } from "../../utils/ignore-console-warnings"; @@ -61,11 +60,7 @@ export async function makeAppSetup( ); app.on("window-all-closed", () => !PLATFORM.IS_MAC && app.quit()); - - // Clean up terminal processes before app quits - app.on("before-quit", () => { - terminalManager.cleanup(); - }); + app.on("before-quit", () => {}); return window; } diff --git a/apps/desktop/src/lib/electron-router-dom.ts b/apps/desktop/src/lib/electron-router-dom.ts index ba9016c5904..4b2526cce98 100644 --- a/apps/desktop/src/lib/electron-router-dom.ts +++ b/apps/desktop/src/lib/electron-router-dom.ts @@ -1,24 +1,13 @@ import { createElectronRouter } from "electron-router-dom"; -// ⚠️ CRITICAL: This module is shared between main and renderer processes -// DO NOT import Node.js modules (fs, path, os, net, etc.) here! -// Doing so will cause "Module externalized for browser compatibility" errors -// If you need Node.js functionality, use IPC or move code to src/main/ +const DEFAULT_PORT = 4927; -// Note: This module can be safely imported in both main and renderer processes -// The port is injected by Vite at build time via import.meta.env.DEV_SERVER_PORT -// Port value comes from: -// 1. Last used port from ~/.superset/dev-port.json (managed by main process) -// 2. Default port 4927 -// The port will automatically switch if unavailable (handled by getPort() async function in main process) - -// Get the port from Vite's import.meta.env, falling back to default const getPort = (): number => { // In renderer process, Vite injects this at build time if (import.meta.env.DEV_SERVER_PORT) { return Number.parseInt(import.meta.env.DEV_SERVER_PORT as string, 10); } - return 4927; // Default fallback + return DEFAULT_PORT; }; export const { Router, registerRoute, settings } = createElectronRouter({ diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index a9af8a02959..a0041bcc922 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -1,41 +1,7 @@ -// Load .env from monorepo root before any other imports -import { existsSync } from "node:fs"; -import { resolve, dirname } from "node:path"; -import { config } from "dotenv"; - -// Find .env file by searching upward from __dirname -// This is robust whether running from source or compiled code -function findEnvFile(): string | undefined { - let currentDir = __dirname; - for (let i = 0; i < 6; i++) { - const envPath = resolve(currentDir, ".env"); - if (existsSync(envPath)) { - return envPath; - } - currentDir = dirname(currentDir); - } - return undefined; -} - -const envPath = findEnvFile(); -if (envPath) { - // Use override: true to ensure .env values take precedence over inherited env vars - config({ path: envPath, override: true }); - console.log(`Loaded .env from ${envPath}`); -} else { - console.warn("No .env file found in parent directories"); -} - import path from "node:path"; import { app } from "electron"; import { makeAppSetup } from "lib/electron-app/factories/app/setup"; -import { registerDeepLinkIpcs } from "main/lib/deep-link-ipcs"; -import { deepLinkManager } from "main/lib/deep-link-manager"; -import { registerPortIpcs } from "main/lib/port-ipcs"; -import { getPort } from "main/lib/port-manager"; -import { registerUiIPCs } from "main/lib/ui-ipcs"; -import windowManager from "main/lib/window-manager"; -import { registerWorkspaceIPCs } from "main/lib/workspace-ipcs"; +import { MainWindow } from "./windows/main"; // Protocol scheme for deep linking const PROTOCOL_SCHEME = "superset"; @@ -52,44 +18,17 @@ if (process.defaultApp) { app.setAsDefaultProtocolClient(PROTOCOL_SCHEME); } -// macOS: Handle deep link when app is already running +// TODO: Handle deep link when app is already running app.on("open-url", (event, url) => { event.preventDefault(); - deepLinkManager.setUrl(url); }); // Allow multiple instances - removed single instance lock -// Each instance will use the same default user data directory -// To use separate data directories, launch with: --user-data-dir=/path/to/custom/dir (async () => { - // Initialize port selection before app starts - // This ensures we get a consistent available port for this workspace - const port = await getPort(); - await app.whenReady(); - // Initialize desktop stores (migration, versioning) before registering IPCs - const { DesktopStores } = await import("main/lib/desktop-stores"); - await DesktopStores.initialize(); - - // Register IPC handlers once at startup (not per-window) - registerWorkspaceIPCs(); - registerPortIpcs(); - registerDeepLinkIpcs(); - registerUiIPCs(); - const { registerWindowIPCs } = await import("main/lib/window-ipcs"); - registerWindowIPCs(); - - await makeAppSetup( - () => windowManager.createWindow(), - () => windowManager.restoreWindows(), - ); + await makeAppSetup(() => MainWindow()); // Stop all periodic rescans when app is quitting - app.on("before-quit", async () => { - const { workspaceRescanManager } = await import( - "main/lib/workspace-rescan" - ); - workspaceRescanManager.stopAll(); - }); + app.on("before-quit", async () => {}); })(); diff --git a/apps/desktop/src/main/lib/menu.ts b/apps/desktop/src/main/lib/menu.ts index 1604bc5141c..2b6a51e8634 100644 --- a/apps/desktop/src/main/lib/menu.ts +++ b/apps/desktop/src/main/lib/menu.ts @@ -1,6 +1,4 @@ import { app, type BrowserWindow, dialog, Menu } from "electron"; -import windowManager from "./window-manager"; -import workspaceManager from "./workspace-manager"; export function createApplicationMenu(mainWindow: BrowserWindow) { const template: Electron.MenuItemConstructorOptions[] = [ @@ -10,25 +8,13 @@ export function createApplicationMenu(mainWindow: BrowserWindow) { { label: "New Window", accelerator: "CmdOrCtrl+Shift+N", - click: async () => { - try { - await windowManager.createWindow(); - } catch (error) { - console.error("[Menu] Failed to create new window:", error); - dialog.showErrorBox( - "Error", - "Failed to create new window: " + - (error instanceof Error ? error.message : "Unknown error"), - ); - } - }, + click: async () => {}, }, { type: "separator" }, { label: "Open Repository...", accelerator: "CmdOrCtrl+O", click: async () => { - // Show directory picker const result = await dialog.showOpenDialog(mainWindow, { properties: ["openDirectory"], title: "Select Repository", @@ -39,72 +25,6 @@ export function createApplicationMenu(mainWindow: BrowserWindow) { } const repoPath = result.filePaths[0]; - - // Get current branch - const worktreeManager = (await import("./worktree-manager")) - .default; - if (!worktreeManager.isGitRepo(repoPath)) { - dialog.showErrorBox( - "Not a Git Repository", - "The selected directory is not a git repository.", - ); - return; - } - - const currentBranch = worktreeManager.getCurrentBranch(repoPath); - if (!currentBranch) { - dialog.showErrorBox( - "Error", - "Could not determine current branch.", - ); - return; - } - - // Check if workspace already exists for this repo - const existingWorkspaces = await workspaceManager.list(); - const existingWorkspace = existingWorkspaces.find( - (ws) => ws.repoPath === repoPath, - ); - - if (existingWorkspace) { - // Workspace already exists, just switch to it - console.log( - "[Menu] Workspace already exists, switching to:", - existingWorkspace, - ); - mainWindow.webContents.send( - "workspace-opened", - existingWorkspace, - ); - return; - } - - // Create workspace with repo name and current branch - const repoName = repoPath.split("/").pop() || "Repository"; - - const createResult = await workspaceManager.create({ - name: repoName, - repoPath, - branch: currentBranch, - }); - - if (!createResult.success) { - dialog.showErrorBox( - "Error", - createResult.error || "Failed to open repository", - ); - return; - } - - // Notify renderer to reload workspaces - console.log( - "[Menu] Sending workspace-opened event:", - createResult.workspace, - ); - mainWindow.webContents.send( - "workspace-opened", - createResult.workspace, - ); console.log("[Menu] Event sent"); }, }, @@ -144,9 +64,6 @@ export function createApplicationMenu(mainWindow: BrowserWindow) { { role: "minimize" }, { role: "zoom" }, { type: "separator" }, - // Custom close handler to prevent Cmd+W from closing the window - // Arc-style behavior: Cmd+W closes tabs/terminals, Cmd+Shift+W closes window - // This allows renderer-side shortcuts to handle Cmd+W for closing content { label: "Close Window", accelerator: "CmdOrCtrl+Shift+W", diff --git a/apps/desktop/src/main/windows/main.ts b/apps/desktop/src/main/windows/main.ts index 6d0c1bcf028..47a39f703ee 100644 --- a/apps/desktop/src/main/windows/main.ts +++ b/apps/desktop/src/main/windows/main.ts @@ -1,20 +1,8 @@ import { join } from "node:path"; import { screen } from "electron"; - import { createWindow } from "lib/electron-app/factories/windows/create"; import { displayName } from "~/package.json"; import { createApplicationMenu } from "../lib/menu"; -import { - type PortClosedEvent, - type PortDetectedEvent, - portDetector, -} from "../lib/port-detector"; -import { registerTerminalIPCs } from "../lib/terminal-ipcs"; -import { - getActiveWorkspaceId, - updateDetectedPorts, -} from "../lib/workspace/workspace-operations"; -import workspaceManager from "../lib/workspace-manager"; export async function MainWindow() { const { width, height } = screen.getPrimaryDisplay().workAreaSize; @@ -33,91 +21,20 @@ export async function MainWindow() { frame: false, titleBarStyle: "hidden", trafficLightPosition: { x: 16, y: 16 }, - webPreferences: { preload: join(__dirname, "../preload/index.js"), webviewTag: true, }, }); - // Register terminal IPCs for this window - const cleanupTerminal = registerTerminalIPCs(window); - - // Set up port detection listeners - portDetector.on("port-detected", async (event: PortDetectedEvent) => { - const { worktreeId } = event; - - // Get detected ports map for this worktree - const detectedPorts = portDetector.getDetectedPortsMap(worktreeId); - - // Find workspace that contains this worktree - const workspaces = await workspaceManager.list(); - for (const workspace of workspaces) { - const worktree = workspace.worktrees.find((wt) => wt.id === worktreeId); - if (worktree) { - // Update detected ports in config - updateDetectedPorts(workspace.id, worktreeId, detectedPorts); - - // Update proxy if this is the active worktree - if (workspace.activeWorktreeId === worktreeId) { - await workspaceManager.updateProxyTargets(workspace.id); - } - break; - } - } - }); - - portDetector.on("port-closed", async (event: PortClosedEvent) => { - const { worktreeId } = event; - - // Get updated detected ports map - const detectedPorts = portDetector.getDetectedPortsMap(worktreeId); - - // Find workspace and update - const workspaces = await workspaceManager.list(); - for (const workspace of workspaces) { - const worktree = workspace.worktrees.find((wt) => wt.id === worktreeId); - if (worktree) { - updateDetectedPorts(workspace.id, worktreeId, detectedPorts); - - // Update proxy if this is the active worktree - if (workspace.activeWorktreeId === worktreeId) { - await workspaceManager.updateProxyTargets(workspace.id); - } - break; - } - } - }); - // Create application menu createApplicationMenu(window); window.webContents.on("did-finish-load", async () => { window.show(); - - // Initialize proxy for active workspace on startup - try { - const activeWorkspaceId = getActiveWorkspaceId(); - - if (activeWorkspaceId) { - const activeWorkspace = await workspaceManager.get(activeWorkspaceId); - - if (activeWorkspace?.ports && activeWorkspace.ports.length > 0) { - await workspaceManager.initializeProxyForWorkspace(activeWorkspaceId); - } - } - } catch (error) { - console.error("[Main] Failed to initialize proxy on startup:", error); - } }); - window.on("close", () => { - // Clean up terminal processes for this window - cleanupTerminal(); - - // Note: Don't destroy other windows - let them close independently - // Each window manages its own lifecycle - }); + window.on("close", () => {}); return window; } diff --git a/apps/desktop/src/renderer/contexts/AppProviders.tsx b/apps/desktop/src/renderer/contexts/AppProviders.tsx index 120a74b2e50..6a43934a1e1 100644 --- a/apps/desktop/src/renderer/contexts/AppProviders.tsx +++ b/apps/desktop/src/renderer/contexts/AppProviders.tsx @@ -1,42 +1,9 @@ import type React from "react"; -import { useState } from "react"; -import { - WorkspaceProvider, - TabProvider, - SidebarProvider, - WorktreeOperationsProvider, - TaskProvider, -} from "./index"; interface AppProvidersProps { children: React.ReactNode; } export function AppProviders({ children }: AppProvidersProps) { - // Tab selection state needs to be lifted to AppProviders level - // so WorkspaceProvider can use it - const [selectedWorktreeId, setSelectedWorktreeId] = useState( - null, - ); - const [selectedTabId, setSelectedTabId] = useState(null); - - return ( - - - - - {children} - - - - - ); + return <>{children}; } diff --git a/apps/desktop/src/renderer/contexts/index.ts b/apps/desktop/src/renderer/contexts/index.ts index c70a05fd1e8..9457e31d208 100644 --- a/apps/desktop/src/renderer/contexts/index.ts +++ b/apps/desktop/src/renderer/contexts/index.ts @@ -1,10 +1 @@ export { AppProviders } from "./AppProviders"; -export { WorkspaceProvider, useWorkspaceContext } from "./WorkspaceContext"; -export { TabProvider, useTabContext } from "./TabContext"; -export { SidebarProvider, useSidebarContext } from "./SidebarContext"; -export { - WorktreeOperationsProvider, - useWorktreeOperationsContext, -} from "./WorktreeOperationsContext"; -export { TaskProvider, useTaskContext } from "./TaskContext"; -export { WorktreeProvider, useWorktree } from "./WorktreeContext"; diff --git a/apps/desktop/src/renderer/screens/main/MainScreen.tsx b/apps/desktop/src/renderer/screens/main/MainScreen.tsx index b5fae125c36..7cad3f94a61 100644 --- a/apps/desktop/src/renderer/screens/main/MainScreen.tsx +++ b/apps/desktop/src/renderer/screens/main/MainScreen.tsx @@ -1,225 +1,15 @@ -import { useState, useEffect } from "react"; import { DndProvider } from "react-dnd"; import { dragDropManager } from "../../lib/dnd"; import { AppFrame } from "./components/AppFrame"; import { Background } from "./components/Background"; -import { AddTaskModal } from "./components/Layout/AddTaskModal"; -import { TaskTabs } from "./components/Layout/TaskTabs"; -import { MainContentArea } from "./components/MainContentArea"; -import { SidebarOverlay } from "./components/SidebarOverlay"; -import { WorkspaceSelectionModal } from "./components/WorkspaceSelectionModal"; -import { - useWorkspaceContext, - useTabContext, - useSidebarContext, - useWorktreeOperationsContext, - useTaskContext, -} from "../../contexts"; -import type { AppMode } from "./types"; -import { enrichWorktreesWithTasks } from "./utils"; -import { createShortcutHandler } from "../../lib/keyboard-shortcuts"; -import { createTabShortcuts } from "../../lib/shortcuts"; export function MainScreen() { - const [mode, setMode] = useState("edit"); - - // Workspace management - const { - workspaces, - currentWorkspace, - loading, - error, - showWorkspaceSelection, - handleWorkspaceSelect, - handleWorkspaceSelectFromModal, - handleCreateWorkspaceFromModal, - } = useWorkspaceContext(); - - // Tab management - const { - selectedWorktreeId, - setSelectedWorktreeId, - selectedWorktree, - selectedTab, - parentGroupTab, - handleTabCreated, - handleTabSelect, - handleTabFocus, - } = useTabContext(); - - // Sidebar management - const { - sidebarPanelRef, - isSidebarOpen, - setIsSidebarOpen, - showSidebarOverlay, - setShowSidebarOverlay, - handleCollapseSidebar, - handleExpandSidebar, - } = useSidebarContext(); - - // Worktree operations - const { - handleWorktreeCreated, - handleUpdateWorktree, - handleCreatePR, - handleMergePR, - handleDeleteWorktree, - } = useWorktreeOperationsContext(); - - // Task management - const { - isAddTaskModalOpen, - addTaskModalInitialMode, - branches, - isCreatingWorktree, - setupStatus, - setupOutput, - pendingWorktrees, - openTasks, - handleOpenAddTaskModal, - handleCloseAddTaskModal, - handleSelectTask, - handleCreateTask, - handleClearStatus, - } = useTaskContext(); - - // Global keyboard shortcuts (window-level, work from anywhere) - useEffect(() => { - const handleCloseTab = async () => { - // Close the currently selected tab (terminal/preview/port) - if (selectedTab && currentWorkspace && selectedWorktreeId) { - try { - const result = await window.ipcRenderer.invoke("tab-delete", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - tabId: selectedTab.id, - }); - - if (result.success) { - // Trigger workspace refresh - window.dispatchEvent(new CustomEvent("workspace-changed")); - } - } catch (error) { - console.error("[MainScreen] Failed to close tab:", error); - } - } - }; - - const tabShortcuts = createTabShortcuts({ - switchToPrevTab: () => { - // TODO: Implement tab switching - }, - switchToNextTab: () => { - // TODO: Implement tab switching - }, - newTab: () => { - // TODO: Implement new tab creation - }, - closeTab: handleCloseTab, - reopenClosedTab: () => { - // TODO: Implement reopen closed tab - }, - jumpToTab: (index: number) => { - // TODO: Implement jump to tab by index - }, - }); - - const handleKeyDown = createShortcutHandler(tabShortcuts.shortcuts); - window.addEventListener("keydown", handleKeyDown); - - return () => { - window.removeEventListener("keydown", handleKeyDown); - }; - }, [selectedTab, currentWorkspace, selectedWorktreeId]); - return ( - - {/* Hover trigger area when sidebar is hidden */} - {!isSidebarOpen && ( - + + + + + ); +} + +export function AppRoutes() { + return ( + } path="/" errorElement={} /> + } + /> + ); +} diff --git a/apps/old-desktop/src/renderer/screens/main/MainScreen.tsx b/apps/old-desktop/src/renderer/screens/main/MainScreen.tsx new file mode 100644 index 00000000000..b5fae125c36 --- /dev/null +++ b/apps/old-desktop/src/renderer/screens/main/MainScreen.tsx @@ -0,0 +1,225 @@ +import { useState, useEffect } from "react"; +import { DndProvider } from "react-dnd"; +import { dragDropManager } from "../../lib/dnd"; +import { AppFrame } from "./components/AppFrame"; +import { Background } from "./components/Background"; +import { AddTaskModal } from "./components/Layout/AddTaskModal"; +import { TaskTabs } from "./components/Layout/TaskTabs"; +import { MainContentArea } from "./components/MainContentArea"; +import { SidebarOverlay } from "./components/SidebarOverlay"; +import { WorkspaceSelectionModal } from "./components/WorkspaceSelectionModal"; +import { + useWorkspaceContext, + useTabContext, + useSidebarContext, + useWorktreeOperationsContext, + useTaskContext, +} from "../../contexts"; +import type { AppMode } from "./types"; +import { enrichWorktreesWithTasks } from "./utils"; +import { createShortcutHandler } from "../../lib/keyboard-shortcuts"; +import { createTabShortcuts } from "../../lib/shortcuts"; + +export function MainScreen() { + const [mode, setMode] = useState("edit"); + + // Workspace management + const { + workspaces, + currentWorkspace, + loading, + error, + showWorkspaceSelection, + handleWorkspaceSelect, + handleWorkspaceSelectFromModal, + handleCreateWorkspaceFromModal, + } = useWorkspaceContext(); + + // Tab management + const { + selectedWorktreeId, + setSelectedWorktreeId, + selectedWorktree, + selectedTab, + parentGroupTab, + handleTabCreated, + handleTabSelect, + handleTabFocus, + } = useTabContext(); + + // Sidebar management + const { + sidebarPanelRef, + isSidebarOpen, + setIsSidebarOpen, + showSidebarOverlay, + setShowSidebarOverlay, + handleCollapseSidebar, + handleExpandSidebar, + } = useSidebarContext(); + + // Worktree operations + const { + handleWorktreeCreated, + handleUpdateWorktree, + handleCreatePR, + handleMergePR, + handleDeleteWorktree, + } = useWorktreeOperationsContext(); + + // Task management + const { + isAddTaskModalOpen, + addTaskModalInitialMode, + branches, + isCreatingWorktree, + setupStatus, + setupOutput, + pendingWorktrees, + openTasks, + handleOpenAddTaskModal, + handleCloseAddTaskModal, + handleSelectTask, + handleCreateTask, + handleClearStatus, + } = useTaskContext(); + + // Global keyboard shortcuts (window-level, work from anywhere) + useEffect(() => { + const handleCloseTab = async () => { + // Close the currently selected tab (terminal/preview/port) + if (selectedTab && currentWorkspace && selectedWorktreeId) { + try { + const result = await window.ipcRenderer.invoke("tab-delete", { + workspaceId: currentWorkspace.id, + worktreeId: selectedWorktreeId, + tabId: selectedTab.id, + }); + + if (result.success) { + // Trigger workspace refresh + window.dispatchEvent(new CustomEvent("workspace-changed")); + } + } catch (error) { + console.error("[MainScreen] Failed to close tab:", error); + } + } + }; + + const tabShortcuts = createTabShortcuts({ + switchToPrevTab: () => { + // TODO: Implement tab switching + }, + switchToNextTab: () => { + // TODO: Implement tab switching + }, + newTab: () => { + // TODO: Implement new tab creation + }, + closeTab: handleCloseTab, + reopenClosedTab: () => { + // TODO: Implement reopen closed tab + }, + jumpToTab: (index: number) => { + // TODO: Implement jump to tab by index + }, + }); + + const handleKeyDown = createShortcutHandler(tabShortcuts.shortcuts); + window.addEventListener("keydown", handleKeyDown); + + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + }, [selectedTab, currentWorkspace, selectedWorktreeId]); + + return ( + + + + {/* Hover trigger area when sidebar is hidden */} + {!isSidebarOpen && ( +