---
name: Dev launcher and scripts
type: architecture-map-page
slice: 05-core-storage-hotkey-build
last_verified: 2026/05/12
---

# Dev launcher and scripts

> **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → Dev launcher and scripts

**Plain English summary.** `run.sh` is the canonical full-stack development launcher and is exposed through `npm run dev:tauri`. It starts Vite first, waits for it to bind port 1420, then runs Tauri with Tauri's own `beforeDevCommand` disabled so a second Vite instance does not race the first one. On Linux it also owns the rendering env-var contract that `src-tauri/src/lib.rs::warn_if_x11_env_unset_on_wayland` checks.

## At a glance

- Files: `run.sh` (+x), `package.json`, `package-lock.json`.
- External tools: `npm`, `npx`, `curl`, `bash` (`run.sh`); the Node toolchain (`package.json` scripts).
- Consumers: developers use `npm run dev:tauri` (canonical) or `./run.sh` (direct shell equivalent). CI uses package scripts such as `npm run build` and `npm run check`.

## `run.sh` — the full-stack dev launcher

```bash
#!/usr/bin/env bash
# Lumotia dev launcher. Starts Vite first, waits for it to bind port 1420,
# then runs Tauri without triggering a second Vite instance.
# Sets the Linux rendering env vars that warn_if_x11_env_unset_on_wayland
# (src-tauri/src/lib.rs) expects the launcher to own.
# For performance testing use: npm run tauri build
set -euo pipefail
cd "$(dirname "$0")"

case "$(uname -s)" in
    Linux)
        # Bindgen libclang path. Fedora 41/LLVM 21 default; user-set wins.
        export LIBCLANG_PATH="${LIBCLANG_PATH:-/usr/lib64/llvm21/lib64}"
        # iGPU idle-cost workaround. Set to 0 explicitly to opt out.
        export WEBKIT_DISABLE_DMABUF_RENDERER="${WEBKIT_DISABLE_DMABUF_RENDERER:-1}"
        if [ "${XDG_SESSION_TYPE:-}" = "wayland" ]; then
            export GDK_BACKEND="${GDK_BACKEND:-x11}"
            export WINIT_UNIX_BACKEND="${WINIT_UNIX_BACKEND:-x11}"
        fi
        ;;
esac

printf 'Starting Vite dev server...\n' >&2
npm run dev:frontend &
VITE_PID=$!

cleanup() {
    kill "$VITE_PID" 2>/dev/null || true
}
trap cleanup EXIT INT TERM

# Wait up to 60s for Vite, bailing if it exits early.
for _ in $(seq 1 120); do
    if curl -sf http://localhost:1420 > /dev/null 2>&1; then
        break
    fi
    if ! kill -0 "$VITE_PID" 2>/dev/null; then
        printf 'Vite dev server exited before becoming ready.\n' >&2
        wait "$VITE_PID"
        exit 1
    fi
    sleep 0.5
done

if ! curl -sf http://localhost:1420 > /dev/null 2>&1; then
    printf 'Timed out waiting for Vite on http://localhost:1420\n' >&2
    exit 1
fi

printf 'Vite ready. Launching Tauri...\n' >&2
npx tauri dev --config '{"build":{"beforeDevCommand":""}}' "$@"
```

### Why this exists

The default `tauri dev` path honours `beforeDevCommand` from `tauri.conf.json` and spawns Vite itself. That is convenient for the simple path, but during interactive iteration it can create a second Vite instance racing for port 1420. The launcher solves this by starting Vite explicitly and disabling Tauri's spawn via `--config '{"build":{"beforeDevCommand":""}}'`.

The launcher is also the dev-time owner of Linux rendering defaults. Rust startup no longer calls `std::env::set_var`; it only warns when the launcher contract was missed.

### Steps

1. `set -euo pipefail` — early exit on any command failure or unset variable.
2. `cd "$(dirname "$0")"` — anchor to the repo root.
3. On Linux, default `LIBCLANG_PATH` to Fedora's LLVM path only if the user has not set it already.
4. On Linux, default `WEBKIT_DISABLE_DMABUF_RENDERER=1`; users can opt out with `WEBKIT_DISABLE_DMABUF_RENDERER=0 ./run.sh`.
5. On Wayland sessions, default `GDK_BACKEND=x11` and `WINIT_UNIX_BACKEND=x11`; user-set values win.
6. Spawn `npm run dev:frontend &`; capture its PID.
7. Install a trap that kills the spawned `npm run dev:frontend` process on launcher exit/interrupt.
8. Poll `curl -sf http://localhost:1420` every 500 ms for up to 60 seconds; if Vite exits early or never becomes ready, fail instead of hanging forever.
9. Run `npx tauri dev` with `beforeDevCommand` disabled and forward any extra args (`./run.sh --release`).

### Linux rendering env-var contract

`src-tauri/src/lib.rs::warn_if_x11_env_unset_on_wayland` is a safety net. It warns when the process starts without env vars that must be set before WebKitGTK/WINIT initialise:

- `WEBKIT_DISABLE_DMABUF_RENDERER=1` — Linux-wide iGPU idle-cost workaround. Set `WEBKIT_DISABLE_DMABUF_RENDERER=0` before launching to opt out.
- `GDK_BACKEND=x11` and `WINIT_UNIX_BACKEND=x11` — only expected when `XDG_SESSION_TYPE=wayland`.

Development uses `run.sh` for that contract. Packaged Linux builds need an equivalent wrapper or `.desktop` `Exec=env` policy; see the residual packaging plan.

### `LIBCLANG_PATH` rationale

Bindgen (pulled by `whisper-rs-sys` and `llama-cpp-sys-2`) needs a `libclang.so` at build time. On Fedora's LLVM 21 packaging, the canonical location is `/usr/lib64/llvm21/lib64`. On Debian-family the path is `/usr/lib/llvm-N/lib`; on macOS Homebrew it's `$(brew --prefix llvm)/lib`. `run.sh` now uses `LIBCLANG_PATH=${LIBCLANG_PATH:-/usr/lib64/llvm21/lib64}` on Linux so Fedora works out of the box while developer-set values win.

## `package.json` — npm scripts and deps

### Scripts

```json
"scripts": {
  "dev": "npm run dev:frontend",
  "dev:frontend": "svelte-kit sync && vite dev",
  "dev:tauri": "./run.sh",
  "build": "vite build",
  "preview": "vite preview",
  "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
  "check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
  "tauri": "tauri"
}
```

- `npm run dev` — frontend only (no Tauri). Useful for pure-UI iteration.
- `npm run dev:tauri` — canonical full-stack dev launcher; calls `./run.sh`.
- `npm run build` — Vite production build. CI gate.
- `npm run check` — `svelte-check` against `jsconfig.json`. CI gate.
- `npm run tauri` — passthrough for non-dev Tauri commands such as `npm run tauri build`.

### Runtime dependencies

- `@tauri-apps/api 2.10.1` — JS bridge to Tauri commands.
- `@tauri-apps/plugin-autostart 2.5.1` — autostart on boot.
- `@tauri-apps/plugin-dialog 2.7.1` — native file dialogs.
- `@tauri-apps/plugin-global-shortcut 2.3.1` — non-Linux hotkey backend (Linux uses our own `lumotia-hotkey` crate, slice 5).
- `@tauri-apps/plugin-notification 2.3.3` — toast notifications.
- `@tauri-apps/plugin-opener 2.x` — open external URLs.
- `@chenglou/pretext 0.0.5` — stylable text preview.
- `lucide-svelte 0.577.0` — icon set.
- `svelte-i18n 4.0.1` — i18n.

### Dev dependencies

- `@sveltejs/kit 2.58.0` + `@sveltejs/adapter-static 3.0.10` + `@sveltejs/vite-plugin-svelte 5.0.0`.
- `@tailwindcss/vite 4.2.1` + `tailwindcss 4.2.1` (Tailwind v4, Vite-plugin-driven, no PostCSS config).
- `@tauri-apps/cli 2.x`.
- `svelte 5.0.0`, `svelte-check 4.0.0`, `typescript 5.6.2`, `vite 6.4.2`.

## Watch-outs

- **The trap kills the spawned npm process, not a full process group.** If Vite-orphan leaks become a real dev annoyance, switch to starting a process group (`setsid`) and killing `-- -$VITE_PID` on Linux.
- **Tailwind v4 is in flight.** Major version, no PostCSS config. Cross-link to slice 1 for the consumed surface.
- **`@tauri-apps/api` is pinned to `2.10.1` (no caret).** Intentional because Tauri's JS bridge can shift behaviour subtly between minors. Pinned ensures we test what we ship.
- **Plugin versions are caret-ranged.** Compatible-change updates land transparently. `npm audit` (CI) catches advisories.

## See also

- [Workspace Cargo.toml](workspace-cargo.md)
- [Frontend build config (vite.config, svelte.config, jsconfig)](frontend-build-config.md)
- [CI pipeline](ci-pipeline.md) — invokes `npm run build` and `svelte-check`.
- [Slice 1 frontend](../01-frontend/README.md) — consumer of every `@tauri-apps/*` runtime dep.
