Skip to content
Merged
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
70 changes: 0 additions & 70 deletions .github/workflows/build-macos-app.yml

This file was deleted.

3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,3 @@ yarn-error.log*

# typescript
*.tsbuildinfo

# staged Tauri backend bundle (built by scripts/prepare-server-bundle.sh)
/server-bundle
31 changes: 2 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,7 @@ Local persistence (SQLite via Prisma) and project CRUD are built and in
use. The app is a Vite React SPA (`client/`) talking to a Bun/Hono
backend (`server/`) — the two run as separate processes, kept in one
piece by `pnpm run dev` from the repo root. This split (no server-side
rendering, one deployable backend unit) is also what makes the native
macOS app possible — see [Download](#download) below — and see the
rendering, one deployable backend unit) keeps deployment simple — see the
[issue tracker](https://github.com/amide-init/transcriptcut/issues)
for the full breakdown of what's done vs. planned.

Expand Down Expand Up @@ -161,39 +160,13 @@ Open [http://localhost:5173](http://localhost:5173). See
[`server/.env.example`](./server/.env.example) for what each variable
does, and [`CONTRIBUTING.md`](./CONTRIBUTING.md) for more.

Two things to know during development:
One thing to know during development:

- **The dev server and the Mac app both use port 3001**, so quit one
before starting the other. If the app shows "404 Not Found", a dev
server is still running.
- **`bun --watch` leaks a few file handles on every reload.** After a long
session with many server edits, FFmpeg can fail to start with
`EBADF: bad file descriptor, posix_spawn`. Restart `pnpm run dev` to
clear it.

### Download

A native macOS app is also available (Apple Silicon only) — no need to
clone the repo or run anything. Grab it from the
**[docs download page](https://transcriptcut.aamin.me/download)**,
which also covers the one-time Gatekeeper step (this build is ad-hoc
signed, not signed with a paid Apple Developer account) and the
first-launch API key setup.

To build it yourself instead:

```bash
cd client
pnpm exec tauri build
```

Produces `client/src-tauri/target/release/bundle/{macos,dmg}/`. Installing
a newer build over an older one keeps your projects: the server upgrades
the app's database on launch. See
`claude.md` section 35 for how it's wired together, and
[`.github/workflows/build-macos-app.yml`](./.github/workflows/build-macos-app.yml)
for how CI builds and releases it on a version tag push.

## Testing

Deterministic unit tests (Vitest) cover the core editing logic — timeline
Expand Down
137 changes: 22 additions & 115 deletions claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -729,15 +729,14 @@ The app is two local processes, not one: a Vite-built React SPA
(`client/`, no server-side rendering) and a Bun/Hono backend (`server/`)
that owns the database, filesystem, FFmpeg, and AI calls. `pnpm run dev`
from the repo root runs both together; in production the built client is
served as static files, optionally by the same Bun process. This split —
no server rendering, one deployable backend unit with a clear entrypoint
— exists so the backend can later run as a Tauri sidecar for a native Mac
app release; nothing about "no cloud account" or "local-first" changes,
it's still one machine, one data directory, no accounts.
served as static files, optionally by the same Bun process
(`CLIENT_DIST_DIR`). This split — no server rendering, one deployable
backend unit with a clear entrypoint — keeps deployment simple; it's
still one machine, one data directory, no accounts.

* **Database:** SQLite via Prisma, using the `@prisma/adapter-libsql`
driver adapter (chosen over `better-sqlite3` specifically to avoid
native-binding packaging pain under Bun/Tauri). Still a single file, no
native-binding packaging pain under Bun). Still a single file, no
server process, no connection string to configure.
* **Storage:** the local filesystem, under a configurable data directory
(see section 15).
Expand Down Expand Up @@ -1475,113 +1474,21 @@ with the same care as the code itself.

---

# 35. Mac App Packaging (Tauri)

A native macOS `.app` build exists at `client/src-tauri/`, wrapping the
Vite client + Bun backend with Tauri v2, built and released via CI
(`.github/workflows/build-macos-app.yml`) and downloadable from the
docs site. **Scope: ad-hoc signed (no paid Apple Developer account),
Apple Silicon (arm64) only, no notarization** — a deliberate choice,
not an oversight: downloaders hit Gatekeeper's "unidentified
developer" warning and need to right-click → Open once (documented on
the download page). Real Developer ID signing + notarization + a
universal (Intel) build are legitimate future steps, not something to
add speculatively.

## Architecture

Rather than a compiled standalone sidecar binary (Tauri's usual
pattern), the app spawns the **system-installed `bun`** to run a staged
copy of the backend, for one specific reason: `bun build --compile`
(the mechanism for a self-contained sidecar binary) has a known,
unresolved upstream bug with `@libsql`, this project's SQLite driver —
confirmed via `oven-sh/bun` issue #18909. Compiling would need a much
bigger rewrite (dropping `@prisma/adapter-libsql` entirely); spawning
the already-installed runtime instead sidesteps the bug completely,
at the cost of requiring Bun to already be on the machine running the
app — a non-issue for this personal-use-only scope.

* `scripts/prepare-server-bundle.sh` stages a self-contained copy of
the backend at repo-root `server-bundle/` (gitignored): server
source (unbundled — Bun runs `.ts` directly), a **hoisted** (flat,
npm-style, zero symlinks) `node_modules`, the generated Prisma
client, the built Vite client, and a pre-migrated empty
`app.db.template`. The node-linker choice matters: pnpm's default
nested/symlinked `node_modules` (even via `pnpm deploy`, which does
correctly resolve the full dependency graph) breaks once Tauri's own
resource-bundler copies it into the `.app` — confirmed empirically
that Tauri's copy step silently drops directories only reachable via
a symlink. `pnpm install --config.node-linker=hoisted` avoids the
problem at the source instead of working around Tauri's copier.
**`server/.env` is deliberately never copied into the bundle** — see
the OpenAI API key note below.
* `client/src-tauri/tauri.conf.json`'s `build.frontendDist` is a URL
(`http://localhost:3001`), not a static path — the packaged app's
window loads directly from the spawned Bun server, which serves both
the built static client and the `/api/*` routes on one origin,
exactly like the dev proxy does. `beforeBuildCommand` builds the
client and runs the staging script (`cd .. && pnpm --filter client
build && bash scripts/prepare-server-bundle.sh`) — Tauri hook
commands run from the frontend package directory (`client/`), so
`cd ..` alone reaches the repo root; an earlier version of this
config had an absolute machine-specific path here instead, which
worked locally but would have failed on any CI runner.
* `client/src-tauri/src/lib.rs`'s `setup()` hook resolves
`resource_dir()`/`app_data_dir()`, seeds the database from
`app.db.template` on first launch only (never overwrites existing
data), spawns `bun run src/index.ts` from the staged bundle via
`tauri-plugin-shell` with `DATABASE_URL`/`DATA_DIR`/`PORT`/
`FFMPEG_PATH` pointed at the right places, creates the main window
hidden and reveals it once the server's own "server listening" log
line appears (avoids a startup race against the window trying to
load a server that isn't up yet), and kills the spawned process on
`RunEvent::Exit` (not `ExitRequested` — a normal macOS quit skips
straight to `Exit`).

## Building it

```bash
cd client
pnpm exec tauri build
```
# 35. Web Only (no native app)

The app ships as the web setup only: `pnpm run dev` locally, or the Bun
server serving the built client in production. A Tauri-wrapped macOS
`.app` (and its CI release workflow) existed earlier and was deliberately
removed to keep the project to one supported setup; don't re-add native
packaging speculatively.

Two pieces that started with the Mac app stay, because they're useful to
any deployment:

Produces both `client/src-tauri/target/release/bundle/macos/AI Video
Editor.app` and a `.dmg` alongside it (`bundle.targets` includes both).
Since it's ad-hoc signed rather than signed with a real Developer ID,
launch it with right-click → Open the first time (a plain double-click
triggers Gatekeeper's "unidentified developer" block) — this applies
whether you built it yourself or downloaded a release.

CI (`.github/workflows/build-macos-app.yml`) runs this same command on
`macos-latest` for every `v*.*.*` tag push, uploads the result as a
workflow artifact on every run (including manual `workflow_dispatch`,
for testing without touching anything public), and additionally
attaches it to the matching GitHub Release when the run was a real tag
push. No Apple secrets needed — ad-hoc signing requires no certificate
or notarization credentials.

## The OpenAI API key

Not baked into the build. Each person who runs the app (whether built
locally or downloaded) is asked for their own key on first launch —
see `server/src/lib/settings.ts` (`DATA_DIR/settings.json`, checked
before falling back to `server/.env`'s `OPENAI_API_KEY` for local dev)
and `client/src/routes/SetupRoute.tsx`. An earlier version of this
build did copy `server/.env` — including a real key — into every
bundle; that was fine while builds never left the developer's own
machine, but had to change once the app became publicly downloadable,
since anyone could otherwise have extracted the key from the bundle.

## Known constraints

* App data lives at `~/Library/Application Support/com.local.aivideoeditor/`,
separate from whatever's in `server/data/` during local dev — the
packaged app starts with an empty project list on first launch, not
a copy of your dev data.
* `FFMPEG_PATH` and the `bun` executable path are hardcoded in
`lib.rs` to the standard Homebrew/Bun install locations on any
Apple Silicon Mac (`/opt/homebrew/opt/ffmpeg-full/bin/ffmpeg`,
`~/.bun/bin/bun`) — not this-machine-specific, but they are real
prerequisites: anyone running the app (built locally or downloaded)
needs Bun and Homebrew's `ffmpeg-full` already installed, documented
on the docs site's download page.
* `server/src/lib/db/migrate.ts` applies pending Prisma migrations at
server startup, so an existing data directory upgrades without the
Prisma CLI.
* The OpenAI API key can be entered on first run through
`client/src/routes/SetupRoute.tsx`, stored in `DATA_DIR/settings.json`
(`server/src/lib/settings.ts`), which is checked before falling back to
`server/.env`'s `OPENAI_API_KEY`.
2 changes: 1 addition & 1 deletion client/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import reactRefresh from "eslint-plugin-react-refresh";
import globals from "globals";

export default defineConfig([
globalIgnores([".next/**", "dist/**", "src/generated/**", "src-tauri/**"]),
globalIgnores([".next/**", "dist/**", "src/generated/**"]),
js.configs.recommended,
...tseslint.configs.recommended,
reactRefresh.configs.vite,
Expand Down
1 change: 0 additions & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
"devDependencies": {
"@eslint/js": "^9.39.5",
"@tailwindcss/postcss": "^4",
"@tauri-apps/cli": "^2.11.5",
"@types/node": "^26.6.2",
"@types/react": "^19",
"@types/react-dom": "^19",
Expand Down
4 changes: 0 additions & 4 deletions client/src-tauri/.gitignore

This file was deleted.

Loading
Loading