Skip to main content

Porting Blitz from macOS/iOS to Windows/Android in two commits

Share:XLinkedInHN
Cover for Porting Blitz from macOS/iOS to Windows/Android in two commits

Credit the upstream first

Blitz is a mobile-dev tool by Minjune Song and Palash Bansal (GitHub: pythonlearner1025). It lives at blitz.dev, ships under Apache 2.0, and its original job was to sit on a Mac, drive an iPhone or a simulator, generate assets, and shepherd a build into the App Store. The upstream repo is Swift with a macOS companion, an iOS worker, and a small bundle of scripts around App Store Connect.

I am on Windows. I ship Android apps. Everything about the upstream stack is off-platform for me. So I forked it: kaushiksaravanan/blitz-windows. The fork stays Apache 2.0, the NOTICE file retains the upstream copyright, and the README credits Minjune and Palash. This post is about the two commits it took to re-aim the tool.

Why fork instead of starting over

The obvious question: if you are throwing out Swift, macOS, and iOS, why not open a blank folder?

Because the interesting parts of Blitz are not the language. They are the IPC shape, the port choices, and the screenshot compositor pattern. Upstream Blitz already had a companion server on a fixed port that the mobile side dials into, a Vite dev harness on port 1420, and a Chrome DevTools Protocol tap on port 9222. That plumbing carries over one-to-one when you swap iOS for Android. The device speaks a different debug protocol, but the room the device stands in is the same room. Keeping the fork also means I can rebase upstream fixes into the desktop controller later.

The two commits

My fork has 41 commits total. 39 are upstream. Two are mine.

The first, 1e545fd on 2026-03-16, is titled "Add cross-platform Android dev toolkit for Windows". It replaces the iOS worker with an Android companion in Kotlin plus Compose and swaps the macOS-only desktop surface out for a Tauri v2 shell so the tool could run on Windows. Tauri picks up a Rust core, which meant a fresh src-tauri/ tree with a 671-line lib.rs, an adb.rs bridge to the Android Debug Bridge, and a companion_server.rs that spoke to the phone.

The second, bb35cf9 on 2026-03-26, is titled "patched". That is a load-bearing four-letter word. In one squashed diff it deletes the entire src-tauri/ tree, throws out the Rust core, deletes the Swift tests and the App Store Connect scripts under scripts/asc-api-tests/* and Tests/blitz_tests/*.swift, deletes Package.swift, and replaces the whole desktop backend with Electron and TypeScript. The Android companion survives. The desktop controller is rewritten end to end.

Because "patched" is squashed, the Tauri-to-Electron rewrite is one opaque diff. I cannot cleanly show which service was ported and which was written from scratch, and I will not pretend every line is hand-written when a rewrite of that size in ten days had AI in the loop for the boring parts.

Why Electron won on Windows

The commit is not the reason. The reason lives in a note I left in CLAUDE.md: "No native Node.js addons, avoids Windows Defender issues." Tauri v2 on Windows ships with WebView2 plus a Rust core, which is usually a nice story. In practice the compiled Rust binary and its bundled sidecars kept getting quarantined by Windows Defender on fresh installs. Unsigned MSI installers with a Rust runtime look, from Defender's angle, like the malware pattern it was tuned to catch.

Electron on Windows has a different reputation with Defender. The stub is signed by the Electron project, the code you ship is JavaScript inside the packaged app, and there is a decade of installer telemetry on Microsoft's side that lets a fresh Electron app boot without an immediate quarantine. Heavier runtime, but it boots the first time.

What the service files do

The rewritten desktop backend lives in src-electron/. There is a main.ts, a preload.ts, and an ipc-handlers.ts that is 742 lines long. Behind those sit fourteen service files that add up to roughly 6,500 lines of TypeScript. The nine largest, and what each carries:

  • adb.ts (596) wraps the Android Debug Bridge binary: device listing, forward/reverse tunnels, shell exec, install and uninstall.
  • companion-server.ts (376) is the Express plus WebSocket server the on-device app dials into. It replaces upstream's iOS companion.
  • emulator.ts (313) drives the Android emulator through AVD and emulator.exe, so a run without a physical Pixel still has a target.
  • ui-automation.ts (1127) is the biggest file. It composes ADB input events, uiautomator dump reads, and Compose-aware selectors into a semi-Playwright-shaped API against a live Android device.
  • screenshot-service.ts (620) is the compositor. Frames off screencap, side-by-side comparison, timestamp overlays, and the strip layouts for docs.
  • video-generator.ts (367) turns screenrecord output into MP4s through ffmpeg.
  • play-store.ts (1124) automates the Google Play Console flow.
  • content-generator.ts (547) generates listing copy, changelogs, and marketing artefacts from the app metadata.
  • genai.ts (497) is the LLM adapter for asset naming, changelog synthesis, and error-message rewrites.

There is no xcrun simctl, no codesign, and no App Store Connect JWT dance. Everything Apple-toolchain-shaped is gone. Everything Google-shaped ended up bigger, because Play Console does not have a first-party CLI on the level of Xcode's.

Play Store automation without stored credentials

The play-store.ts service is the one I want to call out. Upstream Blitz talks to App Store Connect over the JWT-signed REST API. Google's Play Developer API exists, but does not cover the full listing flow, especially the parts under Reach and Devices, testing tracks, or the newer AI content declarations. So the fork drives the Play Console UI.

The trick is that it does not bundle a browser and it does not store credentials. Playwright attaches over CDP to the user's own Chrome, running on localhost:9222 (the same port upstream Blitz used for its browser tap). The user launches Chrome with --remote-debugging-port=9222, signs into their Google account by hand once, and the desktop controller drives the already-authenticated session. No cookies leave the machine. No password ever hits the app. Two-factor still works because you did it as a human.

The Android companion

On the device side, the Kotlin/Compose companion is a small app whose whole job is to talk to the desktop over the LAN. It opens a WebSocket to the companion server on port 9400, exchanges a pairing code with the desktop, registers its capabilities, and streams frames on demand. It also exposes a tiny HTTP surface for large payload uploads such as screenshot bursts and screen recordings. ADB does the debug-bridge role; the companion is where higher-level app-shaped actions live.

The topology

flowchart LR
  A[Windows desktop controller<br/>Electron + TypeScript] -->|IPC| B[ipc-handlers.ts]
  B --> C[adb.ts]
  C -->|USB or TCP| D[Android device or emulator]
  B --> E[companion-server.ts<br/>Express + WS :9400]
  E -->|WebSocket| F[Companion app<br/>Kotlin + Compose]
  F --> D
  B --> G[play-store.ts]
  G -->|CDP :9222| H[User Chrome]
  H --> I[Play Console]

Two rails to the device: ADB for low-level control, the companion WebSocket for app-shaped calls. One rail out to the internet through the user's own browser.

The honest bits

A few things I do not want to overstate.

The Tauri-to-Electron rewrite is one squashed commit. I know what shipped. I cannot show a clean per-file line-by-line history for it.

There is no confirmed release artefact. The repo is set up to build a Windows installer, I have not published a signed one, and I have not asked upstream whether they would take this back as a PR. It is a working source tree I run locally.

And "cross-platform" in the first commit title is too generous. The fork works on Windows targeting Android. Linux is untested and the macOS side of upstream is gone.

Even so, a two-commit history is a nice place to land. One commit to pick the wrong desktop framework, one commit to fix that and rebuild the controller. The Blitz idea holds. The room is the same. The furniture is Windows now.

Cite as: Saravanan, K. (2026). Porting Blitz from macOS/iOS to Windows/Android in two commits. Kaushik Saravanan. https://www.kaushik.cv/blog/blitz-windows-android-port