Skip to content

feat: migrate SproutGit from Tauri/SvelteKit to Electron/React#24

Open
liam-russell wants to merge 3 commits into
mainfrom
electron
Open

feat: migrate SproutGit from Tauri/SvelteKit to Electron/React#24
liam-russell wants to merge 3 commits into
mainfrom
electron

Conversation

@liam-russell
Copy link
Copy Markdown
Contributor

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:

  • Rust compilation times making CI slow and contributor onboarding painful
  • The IPC boundary requiring manual Tauri command definitions in Rust for every operation
  • node-pty and Monaco Editor being difficult to integrate from a Rust sidecar
  • Windows worktree handle-release races requiring fragile workarounds in the E2E teardown

Electron gives us a single TypeScript/Node.js process on both sides of the IPC boundary, native node-pty support, Monaco Editor as a first-class npm package, and node:sqlite built into Electron ≥ 32 (no native binary dependencies at all).


Commit-by-commit breakdown

c112a32 — feat: migrate SproutGit app to Electron + React monorepo

The core migration. Establishes the monorepo structure:

app/               ← Electron main + preload + renderer (electron-vite)
packages/
  git/             ← @sproutgit/git — simple-git wrapper
  terminal/        ← @sproutgit/terminal — node-pty TerminalManager
  database/        ← @sproutgit/database — Drizzle ORM + node:sqlite
  types/           ← @sproutgit/types — all IPC channel constants + shared types
  ui/              ← @sproutgit/ui — shared React components

Key design decisions:

  • nodeIntegration: false + contextIsolation: true maintained throughout — all Node access goes through the contextBridge (window.api)
  • IPC contract: every channel name lives in packages/types/src/ipc.ts as IPC.DOMAIN_ACTION = 'domain:action'; the preload exposes typed wrappers; the renderer never touches ipcRenderer directly
  • node:sqlite (Electron-bundled) replaces better-sqlite3 — zero native binaries, no rebuild step
  • Drizzle ORM with two DB files: config.db (user settings, recent workspaces) and state.db (per-workspace worktrees, hooks, metadata)
  • TanStack Router v1 with createHashHistory() for the renderer SPA
  • Zustand v5 for renderer state (useWorkspaceStore, useUpdateStore)
  • Tailwind CSS v4 with CSS variables prefixed --sg-*; no config file
  • electron-log for structured logging to disk on all platforms
  • electron-updater for auto-update

Feature-complete port of all Tauri commands:

  • Git operations (stage, unstage, commit, diff, log/graph, branches, remotes, worktrees)
  • Workspace lifecycle (import, init, inspect, recent workspaces, open-file event)
  • Terminal (node-pty pool keyed by (workspacePath, worktreePath))
  • Settings (persisted to config DB)
  • GitHub OAuth + repo listing
  • Hooks (create/read/update/delete, shell execution with env injection, progress events)
  • Filesystem watcher (chokidar → push events to renderer)
  • macOS application menu (required for Cmd+C/V/Z/X in text inputs)
  • app.addRecentDocument() on every workspace open

02e956d — fix: dialog centering, auto-activate first worktree, and disable tabs without active worktree

Post-migration polish:

  • New worktree dialog is now centred in the window
  • First worktree is automatically activated when a workspace opens
  • Workspace tab bar items are disabled (cursor + pointer-events) when no active worktree is selected

af8675b — refactor: replace OpenTelemetry/Aspire with electron-log for disk logging

The 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:

  • Main process: log exported from app/src/main/telemetry.tslog.info/warn/error() everywhere
  • Renderer: all console.* calls are forwarded to the log file via the console-message event in createWindow()
  • Log file locations: ~/Library/Logs/SproutGit/main.log (macOS), %APPDATA%\SproutGit\logs\main.log (Windows)

0abad62 — feat: overhaul CI/CD, E2E framework, and developer experience

Replaced 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 via wdio-electron-service
  • e2e/helpers.tscreateTestRepo() helper spins up a real .sproutgit/ workspace in a temp dir; gotoHash() navigates the SPA via window.location.hash
  • 6 spec files covering: smoke, import workflow, commit workflow, daily workflow (parallel worktrees), worktree workflow, and hero screenshots
  • All 5 specs pass (hero screenshots spec is skipped unless @screenshots tag is set)

CI changes:

  • turbo.json pipeline: typecheck → build → test with proper cache keys
  • oxlint config (.oxlintrc.json) — fast Rust-based linting alongside TypeScript typecheck
  • pnpm typecheck runs tsgo (TypeScript Go port) across all packages for sub-second typechecks

f95aff0 — 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:

  1. TERMINAL_CLOSE_ALL IPCTerminalManagerWithMeta.closeAll() method + IPC handler so the renderer can tear down all PTY sessions at once (e.g. on workspace close)

  2. 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:

    SPROUTGIT_WORKSPACE, SPROUTGIT_WORKSPACE_NAME,
    SPROUTGIT_ROOT_PATH, SPROUTGIT_WORKTREES_PATH,
    SPROUTGIT_WORKTREE, SPROUTGIT_WORKTREE_NAME,
    SPROUTGIT_WORKTREE_BRANCH, SPROUTGIT_SOURCE_REF,
    SPROUTGIT_TRIGGER, SPROUTGIT_INITIATING_WORKTREE,
    SPROUTGIT_HOOK_ID, SPROUTGIT_HOOK_NAME,
    SPROUTGIT_HOOK_SCOPE, SPROUTGIT_HOOK_SHELL,
    SPROUTGIT_HOOK_CRITICAL, SPROUTGIT_HOOK_TIMEOUT_SECONDS,
    SPROUTGIT_OS
    

    The WorkspaceHooksModal reference panel was updated to show all 17 variables grouped by category (Workspace / Worktree / Trigger / Hook / System).

  3. 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.

  4. Hook dependency DAG — The old app had a "Run after" dependency selector in the hooks modal, backed by a hookDependencies join table. Ported fully:

    • hookDependencies table in the workspace Drizzle schema
    • HOOK_LIST IPC now joins and populates dependencyIds: string[] on each hook
    • HOOK_CREATE / HOOK_UPDATE IPC insert/replace dependency rows
    • WorkspaceHooksModal shows a "Run after" checkbox list for hooks sharing the same trigger

938484d — 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:

Check Result
pnpm typecheck (tsgo, all packages) ✅ 6/6 cached pass
pnpm test (vitest, git + database + ui) ✅ 29 tests pass
pnpm test:e2e (wdio + Electron) ✅ 9 tests pass, 1 skipped (screenshots)
oxlint ✅ 0 errors (5 pre-existing warnings)

Security

  • nodeIntegration: false and contextIsolation: true enforced throughout
  • All user input passed to git/fs APIs goes through simple-git typed APIs — no shell string interpolation
  • IPC path arguments are validated before filesystem use
  • No credentials in source; GitHub OAuth token stored in Electron safeStorage

Copilot AI review requested due to automatic review settings May 14, 2026 02:51
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

Comment thread app/src/main/index.ts Fixed
Comment thread app/src/main/ipc/hooks.ts Fixed
Comment thread app/src/renderer/workspace/dialogs/DeleteWorktreeDialog.tsx Fixed
Comment thread e2e/specs/commit-workflow.spec.ts Fixed
Comment thread packages/ui/src/components/CommitGraph.tsx Fixed
Copy link
Copy Markdown
Contributor Author

@liam-russell liam-russell left a comment

Choose a reason for hiding this comment

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

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.jsonpublish.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.tsensureWorkspaceHookColumns 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.tsshell.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.tscore.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.tshook_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.ts line 115require('os').homedir() is CJS in an ESM file. Use import { homedir } from 'os'. (inline comment)
  • app/package.json copyright"Copyright © 2025" should be 2026.
  • packages/terminal/src/terminal-manager.tsTerminalManager.closeForPath body is dead code (void id; void session with a comment redirecting to the subclass). Either make the base method abstract or remove the dead loop.
  • packages/database/src/schema/config.tssettings.value is nullable (text('value')) but every read-path assumes it's defined. Add .notNull() to match actual usage.
  • GitHub OAuth scopescope: 'repo user:email' gives full write access to all repos. Consider read:user user:email for auth-only, and add repo only 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.ts is a clean shim for drizzle-orm/better-sqlite3 over node:sqlite. The transaction wrapper handles BEGIN DEFERRED/IMMEDIATE/EXCLUSIVE correctly.
  • Hook execution in hooks.ts uses spawn(..., { shell: false }) with a fixed [cmd, args, script] argv — no shell injection surface.
  • Terminal manager's resolveShellBin correctly rejects shell names with embedded arguments before they reach node-pty.
  • safeStorage usage for the GitHub token is correct; the plaintext fallback on Linux (when libsecret isn't available) is the best available option.

Comment thread app/src/main/ipc/system.ts
Comment thread app/src/main/ipc/system.ts
Comment thread app/src/main/ipc/system.ts
Comment thread packages/git/src/worktrees.ts Outdated
Comment thread packages/database/src/workspace-db.ts Outdated
Comment thread packages/database/src/schema/workspace.ts
Comment thread app/package.json
liam-russell added a commit that referenced this pull request May 14, 2026
- 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
@liam-russell liam-russell requested a review from marktimmins May 14, 2026 03:17
- 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants