Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ lerna-debug.log*

node_modules
dist
dist-agent
dist-desktop
dist-ssr
*.local

Expand Down
29 changes: 27 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,12 @@ curlit/
│ ├── ARCHITECTURE.md
│ └── USER_GUIDE.md
├── e2e/ # Playwright end-to-end tests
├── electron/ # Electron desktop shell
│ ├── main.cjs # Main process (window + IPC wiring)
│ ├── preload.cjs # contextBridge exposing window.curlit
│ └── ipc.cjs # Port of proxy.js logic as IPC handlers
├── server/
│ ├── proxy.js # Express proxy to bypass CORS
│ ├── proxy.js # Express proxy to bypass CORS (browser build only)
│ └── __tests__/ # Proxy server tests
├── src/
│ ├── components/ # React components
Expand Down Expand Up @@ -97,6 +101,25 @@ curlit/
| `npm run test:watch` | Run tests in watch mode |
| `npm run test:coverage` | Run tests with V8 coverage report |
| `npm run test:e2e` | Run Playwright end-to-end tests |
| `npm run electron:dev` | Start Vite + the Electron desktop app in dev mode |
| `npm run electron:build` | Build the desktop app for the current platform |
| `npm run electron:build:win` / `:mac` / `:linux` | Build the desktop app for a specific target |

## Desktop App

CurlIt ships as an Electron desktop app that bypasses the browser's CORS model entirely. Requests, WebSockets, and OAuth token exchanges run in the Electron main process over IPC — no proxy server to manage.

```bash
# Dev mode (hot reload + DevTools)
npm run electron:dev

# Production build for your current platform
npm run electron:build
```

Installers are emitted to `dist-desktop/`. GitHub sync inside the desktop app still reads `GITHUB_CLIENT_ID` from the environment, the same way the proxy does.

See [docs/DESKTOP.md](docs/DESKTOP.md) for full build instructions, IPC architecture, and troubleshooting.

## Testing

Expand All @@ -123,7 +146,9 @@ npm run test:e2e

## How It Works

CurlIt runs a lightweight Express proxy server alongside the Vite dev server. When you send a request, the frontend POSTs the request configuration to `/api/proxy`, which forwards it to the target API using Node.js `fetch`. This avoids browser CORS restrictions and returns the full response (status, headers, body, cookies) back to the UI.
In the browser build, CurlIt runs a lightweight Express proxy server alongside the Vite dev server. When you send a request, the frontend POSTs the request configuration to `/api/proxy`, which forwards it to the target API using Node.js `fetch`. This avoids browser CORS restrictions and returns the full response (status, headers, body, cookies) back to the UI.

In the desktop build, the same request payload is sent over Electron IPC to the main process instead -- the Node runtime in Electron has no CORS, so there's no proxy server to run. Everything else (WebSocket relay, OAuth token exchange, GitHub device flow) works identically via IPC.

All data (collections, environments, history, panel sizes) is persisted in `localStorage` -- no database or account required.

Expand Down
107 changes: 107 additions & 0 deletions docs/DESKTOP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# CurlIt Desktop App

CurlIt ships as an Electron desktop app. Unlike the browser build, the desktop
app needs no proxy server -- HTTP requests, WebSocket connections, OAuth token
exchanges, and the GitHub device-flow sign-in all run inside the Electron main
process (Node.js), which has no CORS restrictions.

The renderer (the React UI) is the same code as the browser build. A small
runtime check (`isDesktop()` in [`src/utils/desktop.ts`](../src/utils/desktop.ts))
decides whether to dispatch over IPC or to the Express proxy.

## Run in development

```bash
npm install
npm run electron:dev
```

This starts Vite on `http://localhost:5173` and launches Electron pointing at
that URL via `CURLIT_DEV_URL`. DevTools open in detached mode automatically.
Hot-reload works for the renderer; if you change anything in `electron/`,
restart the script.

## Build a desktop installer

```bash
# Current platform
npm run electron:build

# Or target a specific OS
npm run electron:build:win
npm run electron:build:mac
npm run electron:build:linux
```

Installers are emitted to `dist-desktop/`:

- **Windows** -- NSIS installer (`.exe`) with desktop shortcut + custom install
path
- **macOS** -- universal `.dmg` (x64 + arm64)
- **Linux** -- `AppImage` and `.deb`

Cross-platform builds work from any host but require platform-specific signing
toolchains for distribution. For local testing the unsigned binary in
`dist-desktop/<platform>-unpacked/` runs as-is.

## Architecture

```
electron/
├── main.cjs Main process: BrowserWindow + IPC wiring
├── preload.cjs contextBridge exposing window.curlit to the renderer
└── ipc.cjs Handlers ported from server/proxy.js
```

The renderer talks to the main process through a single object,
`window.curlit`, defined in [`src/utils/desktop.ts`](../src/utils/desktop.ts):

| Method | Replaces |
|---|---|
| `http(payload)` | `POST /api/proxy` |
| `oauthToken(payload)` | `POST /api/oauth/token` |
| `githubStatus()` | `GET /api/github/status` |
| `githubDeviceCode()` | `POST /api/github/device-code` |
| `githubDeviceToken(code)` | `POST /api/github/device-token` |
| `wsConnect / wsSend / wsClose / onWsEvent` | `ws://.../api/ws-proxy` relay |

Payload shapes are intentionally identical to what the Express endpoints accept,
so the call sites only differ in transport.

## Configuring GitHub sync in the desktop build

The desktop `githubStatus`/`githubDeviceCode`/`githubDeviceToken` handlers read
`process.env.GITHUB_CLIENT_ID` -- same as the proxy server. To enable GitHub
sync in the packaged app, launch it with the env var set, e.g. on macOS:

```bash
GITHUB_CLIENT_ID=Iv1.xxxxxxxxxxxxxxxx open -a CurlIt
```

If the env var is unset, the GitHub sync UI gracefully reports
"GITHUB_CLIENT_ID not configured" and the rest of the app works normally.

## Why Electron and not Tauri

Tauri produces a smaller binary (~10 MB vs Electron's ~200 MB), but CurlIt
already has a mature Node.js proxy (Express + undici + `ws` + manual multipart
construction with proper boundary handling). Porting that to Rust would have
been a multi-week rewrite for cosmetic gains. Electron lets the same code run
in both environments with a thin transport-shim, which is the right trade for
a v1.

## Troubleshooting

**Desktop window opens but the page is blank.** Check that `dist/index.html`
exists. The desktop build expects a production renderer bundle; run
`npm run electron:build` (which calls `vite build --base=./` first) rather than
`vite build` -- the relative base path is required so assets resolve under the
`file://` protocol.

**Requests hang or show "Failed to fetch" in dev mode.** Vite is probably not
running yet. The `electron:dev` script waits on TCP 5173 before launching
Electron; if you launched Electron manually, start Vite first.

**`app is undefined` when launching `electron electron/main.cjs`.** The shell
has `ELECTRON_RUN_AS_NODE=1` set in its environment, which forces Electron to
run as plain Node. Unset it (`unset ELECTRON_RUN_AS_NODE`) and retry.
2 changes: 1 addition & 1 deletion docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@

### v1.5 -- Platform & Ecosystem

- [ ] Electron or Tauri desktop app (no proxy server needed)
- [x] Electron desktop app (no proxy server needed)
- [ ] CLI tool (`curlit run collection.json`) for CI/CD pipelines
- [ ] VS Code extension
- [ ] Plugin system for community extensions
Expand Down
38 changes: 38 additions & 0 deletions electron-builder.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
appId: com.curlit.app
productName: CurlIt
copyright: Copyright © 2026 CurlIt
directories:
output: dist-desktop
buildResources: public
files:
- electron/**/*
- dist/**/*
- package.json
- '!node_modules/**/*'
extraMetadata:
# electron-builder reads `main` from package.json; we point it at the CJS entry.
main: electron/main.cjs
win:
target:
- target: nsis
arch:
- x64
icon: logo.png
mac:
target:
- target: dmg
arch:
- x64
- arm64
icon: logo.png
category: public.app-category.developer-tools
linux:
target:
- AppImage
- deb
icon: logo.png
category: Development
nsis:
oneClick: false
allowToChangeInstallationDirectory: true
createDesktopShortcut: true
Loading