feat: migrate SproutGit from Tauri/SvelteKit to Electron/React#24
feat: migrate SproutGit from Tauri/SvelteKit to Electron/React#24liam-russell wants to merge 3 commits into
Conversation
liam-russell
left a comment
There was a problem hiding this comment.
Review: Tauri → Electron migration (PR #24)
This is a thorough, well-structured rewrite. The architecture decisions are solid: typed IPC contract via IpcMap, strict contextIsolation: true + nodeIntegration: false throughout, safeStorage for GitHub tokens, execFile (not exec) for process spawning, all git operations isolated in @sproutgit/git, and a clean Drizzle migration stack. All CI checks pass. The main blockers are a broken publish config, one security concern, and two data-correctness bugs.
Blockers
app/package.json — publish.owner/publish.repo are wrong (inline comment on file)
electron-updater will 404 on every update check. Values must be InterestingSoftware/SproutGit.
packages/database/src/workspace-db.ts — ensureWorkspaceHookColumns raw SQL (inline comment)
Hand-written ALTER TABLE bypasses drizzle-kit and breaks the migration chain. Replace with a proper migration generated by drizzle-kit generate.
packages/git/src/worktrees.ts — branch name deduced from worktree path (inline comment)
worktreePath.split('/').at(-1) silently produces the wrong branch name for any branch containing / (e.g. feature/my-thing). The branch name is already known at the call site — pass it in explicitly.
Security
app/src/main/ipc/system.ts — shell.openExternal with no URL validation (inline comment)
Any URI scheme is accepted. Should reject anything that isn't https: or http:.
app/src/main/ipc/system.ts — core.editor flags not split before execFile (inline comment)
"code --wait" and similar editor values will fail because the full string is used as the executable name.
Data integrity
packages/database/src/schema/workspace.ts — hook_dependencies has no PK (inline comment)
Duplicate edges can be inserted. Needs a composite PK on (hook_id, depends_on_id).
Minor / nits
app/src/main/ipc/system.tsline 115 —require('os').homedir()is CJS in an ESM file. Useimport { homedir } from 'os'. (inline comment)app/package.jsoncopyright —"Copyright © 2025"should be 2026.packages/terminal/src/terminal-manager.ts—TerminalManager.closeForPathbody is dead code (void id; void sessionwith a comment redirecting to the subclass). Either make the base methodabstractor remove the dead loop.packages/database/src/schema/config.ts—settings.valueis nullable (text('value')) but every read-path assumes it's defined. Add.notNull()to match actual usage.- GitHub OAuth scope —
scope: 'repo user:email'gives full write access to all repos. Considerread:user user:emailfor auth-only, and addrepoonly when actually cloning private repos.
What's good
- Typed IPC contract (
IpcMap) is excellent — can't accidentally call the wrong channel or mistype args. node-sqlite-compat.tsis a clean shim fordrizzle-orm/better-sqlite3overnode:sqlite. The transaction wrapper handlesBEGIN DEFERRED/IMMEDIATE/EXCLUSIVEcorrectly.- Hook execution in
hooks.tsusesspawn(..., { shell: false })with a fixed[cmd, args, script]argv — no shell injection surface. - Terminal manager's
resolveShellBincorrectly rejects shell names with embedded arguments before they reachnode-pty. safeStorageusage for the GitHub token is correct; the plaintext fallback on Linux (whenlibsecretisn't available) is the best available option.
- Remove unused imports: nativeImage (main/index.ts), execSync (commit-workflow.spec.ts), randomUUID (hooks.ts)
- Remove unused variables: primaryBtn (DeleteWorktreeDialog), branchToWorktreePath (CommitGraph), runId (hooks.ts)
- system.ts: use static homedir() import instead of require('os'), split editor cmd on whitespace for args with flags, validate http/https scheme before shell.openExternal
- worktrees.ts: add branchName param to deleteManagedWorktree; derive from caller not from path segment (supports branches with slashes)
- Update IPC type, git.ts handler, preload, and workspace.tsx to pass branchName
- DB schema: add composite PK to hook_dependencies table; settings.value NOT NULL
- Remove ensureWorkspaceHookColumns inline ALTER TABLE hack; rely on migrations
- Generate drizzle migrations for both schema changes
- terminal-manager.ts: remove dead code in closeForPath base method
- app/package.json: copyright year 2025 -> 2026
- github.ts: reorder OAuth scope to read:user user:email repo
- Migrate app from Tauri/SvelteKit to Electron 42 + React 19 - Replace OpenTelemetry/Aspire with electron-log for persistent logging - Port all missing features from old app (hooks, settings, worktree UX) - Remove old/ Tauri source - Overhaul CI/CD pipeline with Turborepo, pnpm workspaces, electron-builder - Add WebdriverIO E2E test suite (wdio + wdio-electron-service) - Fix cross-platform CI issues: Windows EBUSY, SQLite file handles, path normalisation - Fix Linux E2E: guard startUpdateCheck behind isE2EMode to prevent D-Bus deadlock - Fix Linux headless rendering: xvfb-run, sandbox flags, apt deps - Add 4x timeout headroom for Linux CI runners - Document Linux CI gotchas in copilot-instructions
…d fix - Windows: use titleBarStyle hidden + null menu for native frame-free experience - WindowControls: add side prop for correct left/right placement on each platform - Routes: update titlebars on index, workspace, and settings routes - GitHub: extract API logic into @sproutgit/provider-github package - ipc/github.ts: thin IPC layer; delegate to provider, keep safeStorage here - electron.vite.config.ts: add provider-github to workspace bundles + hot reload - pnpm-workspace.yaml: add packages/providers/* glob - Website: pin @tailwindcss/vite@4.2.4 + vite@^7.3.2 to avoid Vite 8 hoisting
- Restructure screenshot assets: flat category folders -> {mac,windows,linux}/{category}/
- E2E helper: auto-write to platform subfolder based on process.platform
- E2E helper: skip macOS traffic-light injection on Windows/Linux
- ScreenshotSlider: glob-import all PNGs, optimise all 3 platform variants at build time
- ScreenshotSlider: inline JS detects visitor OS via userAgent, swaps source srcsets
before browser fetches images - stacks with prefers-color-scheme dark/light swap
- windows/ and linux/ seeded with mac screenshots as placeholder until native shots are taken
Overview
This PR is the complete rewrite of SproutGit from a Tauri + SvelteKit desktop app into a pure Electron + React monorepo. Every feature from the old implementation has been ported, validated, and in several areas improved. The legacy
old/source tree has been removed at the end of this branch.Why Electron?
The Tauri implementation was constrained by:
node-ptyand Monaco Editor being difficult to integrate from a Rust sidecarElectron gives us a single TypeScript/Node.js process on both sides of the IPC boundary, native
node-ptysupport, Monaco Editor as a first-class npm package, andnode:sqlitebuilt into Electron ≥ 32 (no native binary dependencies at all).Commit-by-commit breakdown
c112a32— feat: migrate SproutGit app to Electron + React monorepoThe core migration. Establishes the monorepo structure:
Key design decisions:
nodeIntegration: false+contextIsolation: truemaintained throughout — all Node access goes through the contextBridge (window.api)packages/types/src/ipc.tsasIPC.DOMAIN_ACTION = 'domain:action'; the preload exposes typed wrappers; the renderer never touchesipcRendererdirectlynode:sqlite(Electron-bundled) replacesbetter-sqlite3— zero native binaries, no rebuild stepconfig.db(user settings, recent workspaces) andstate.db(per-workspace worktrees, hooks, metadata)createHashHistory()for the renderer SPAuseWorkspaceStore,useUpdateStore)--sg-*; no config fileFeature-complete port of all Tauri commands:
open-fileevent)(workspacePath, worktreePath))app.addRecentDocument()on every workspace open02e956d— fix: dialog centering, auto-activate first worktree, and disable tabs without active worktreePost-migration polish:
af8675b— refactor: replace OpenTelemetry/Aspire with electron-log for disk loggingThe initial migration brought over an OpenTelemetry/Aspire observability stack (originally for the Tauri app). Replaced with
electron-log, which is appropriate for a desktop app:logexported fromapp/src/main/telemetry.ts—log.info/warn/error()everywhereconsole.*calls are forwarded to the log file via theconsole-messageevent increateWindow()~/Library/Logs/SproutGit/main.log(macOS),%APPDATA%\SproutGit\logs\main.log(Windows)0abad62— feat: overhaul CI/CD, E2E framework, and developer experienceReplaced the Tauri Playwright adapter with a purpose-built WebdriverIO + Electron ChromeDriver E2E framework:
e2e/wdio.conf.ts— builds the app with electron-vite before each run, then launches viawdio-electron-servicee2e/helpers.ts—createTestRepo()helper spins up a real.sproutgit/workspace in a temp dir;gotoHash()navigates the SPA viawindow.location.hash@screenshotstag is set)CI changes:
turbo.jsonpipeline:typecheck → build → testwith proper cache keys.oxlintrc.json) — fast Rust-based linting alongside TypeScript typecheckpnpm typecheckrunstsgo(TypeScript Go port) across all packages for sub-second typechecksf95aff0— feat: port missing old-app features before removing old/Pre-deletion gap analysis identified 4 features present in the old Svelte app but missing from the new implementation:
TERMINAL_CLOSE_ALLIPC —TerminalManagerWithMeta.closeAll()method + IPC handler so the renderer can tear down all PTY sessions at once (e.g. on workspace close)Hook environment variable expansion — The old app injected 17 named env vars into hook shell scripts. The new app only injected 4. Expanded to the full set:
The
WorkspaceHooksModalreference panel was updated to show all 17 variables grouped by category (Workspace / Worktree / Trigger / Hook / System).First-worktree onboarding panel — When a workspace has no worktrees, the sidebar now shows a guided empty state with a "Create first worktree" CTA button instead of bare text.
Hook dependency DAG — The old app had a "Run after" dependency selector in the hooks modal, backed by a
hookDependenciesjoin table. Ported fully:hookDependenciestable in the workspace Drizzle schemaHOOK_LISTIPC now joins and populatesdependencyIds: string[]on each hookHOOK_CREATE/HOOK_UPDATEIPC insert/replace dependency rowsWorkspaceHooksModalshows a "Run after" checkbox list for hooks sharing the same trigger938484d— chore: remove old Tauri/SvelteKit source (old/)142 files, ~30k lines of Rust + SvelteKit source removed. The old implementation is fully superseded.
Test results
All checks pass on
HEAD:pnpm typecheck(tsgo, all packages)pnpm test(vitest, git + database + ui)pnpm test:e2e(wdio + Electron)oxlintSecurity
nodeIntegration: falseandcontextIsolation: trueenforced throughoutsimple-gittyped APIs — no shell string interpolationsafeStorage