Development setup
First-time setup and the everyday two-terminal dev loop — backend on :1112, Vite on :1515, each half hot-reloading.
This page takes you from a clean checkout to a running dev environment with hot reload on both halves of the app. The everyday loop is two terminals: the Python backend on :1112 and the Vite dev server on :1515, each side reloading its own code.
First-time setup
Run these once. They install dependencies for both halves and write your local config.
# Python deps (reads pyproject.toml + uv.lock)
uv sync
# Node deps
cd ui
npm install
cd ..
# Initial config: profile, LLM keys, ports, etc.
uv run cremind setupuv run cremind setup is interactive and writes ~/.cremind/ with your profile and provider credentials. You only need to run it again if you delete ~/.cremind/.
ui/node_modules is large
The UI's node_modules is roughly 600 MB. The first npm install takes a while — that's expected, not a hang.
The two-terminal loop
The daily loop runs the backend and the Vite dev server side by side. The backend serves the API on :1112; Vite serves the UI on :1515 with hot module replacement (HMR).
Prerequisite: free port :1515 for Vite
uv run cremind serve opens a second listener on :1515 to serve the bundled SPA from app/static/ui/ — but only if that directory contains an index.html. In a fresh checkout it's empty, so the listener stays silent and leaves :1515 for Vite.
The trap: if you've ever run bash scripts/build_ui.sh (for example, for a release smoke test), app/static/ui/ is populated. The backend will then fight Vite for :1515, and you'll be looking at a stale build with no HMR.
Pick one of these before starting Terminal A:
# Option A (simplest): wipe the build artifact. It's gitignored;
# rebuild anytime with bash scripts/build_ui.sh.
Remove-Item -Recurse -Force app\static\ui
# Option B: keep the build, but disable the SPA listener this session.
$env:CREMIND_UI_PORT = "0"
# Option C: keep the build, point the listener at a nonexistent path.
$env:CREMIND_UI_DIR = "C:\nonexistent"If you skip this and the backend wins :1515, Terminal A logs SPA listener: http://127.0.0.1:1515 (serving …\app\static\ui). That line is the warning sign. Apply one of the options above and restart Terminal A.
Terminal A — backend
uv run cremind serveThe API listens on :1112. With the prerequisite applied, Terminal A logs SPA not present at …\app\static\ui; UI listener disabled and leaves :1515 for Vite.
Terminal B — Vite dev server
cd ui
npm run web:devOpen http://localhost:1515. The port-swap heuristic in ui/src/services/runtimeConfig.ts detects the :1515 host and auto-resolves the backend at :1112 — you don't need to set VITE_AGENT_URL.
To confirm you're hitting Vite and not a stale backend bundle, open browser DevTools and look at the Sources panel. You should see /@vite/client — that's HMR live. If you only see a hashed file like assets/index-Cabc123.js, the backend's SPA listener won the port; revisit the prerequisite above.
What hot-reloads
| Change | How to pick it up |
|---|---|
ui/src/** (Vue, TS, CSS) | Vite HMR — instant in the browser. |
ui/vite.config*.ts, ui/package.json | Restart Terminal B (Ctrl+C, then npm run web:dev). |
app/** (Python) | Restart Terminal A. There is no --reload flag on cremind serve today. |
pyproject.toml deps | uv sync --all-extras, then restart Terminal A. |
app/__version__.py | The pre-commit hook syncs ui/package.json automatically. Manual: python scripts/sync_ui_version.py. |
Optional: auto-restart on Python edits
cremind serve doesn't expose --reload, but you can wrap it with watchfiles from the outside:
uv run --with watchfiles watchfiles "uv run cremind serve" appThis watches app/ and restarts the whole process on any change. It's slower than an in-process reload — roughly a couple of seconds for a cold restart — but it always works.
Variations
| You want… | Run |
|---|---|
| Backend only, no UI | uv run cremind serve. The UI listener on :1515 stays silent. |
| UI pointed at a remote backend | cd ui ; npm run web:dev. Set the agent URL via the setup wizard, or $env:VITE_AGENT_URL = "https://..." before npm run web:dev. |
| Single-port end-to-end smoke (no HMR) | bash scripts/build_ui.sh ; uv run cremind serve. The SPA is bundled into app/static/ui/ and served on :1515 by the backend. Use this for pre-release verification, not active dev. |
| Electron desktop dev | cd ui ; npm run dev. Wraps the SPA in an Electron window; it talks to the backend URL from ~/.cremind-ui/cremind-config.json. |
Gotchas
- Stale UI on
:1515after a previousbuild_ui.sh. See the prerequisite above. Symptom in Terminal A: theSPA listener: …log line. Symptom in the browser: edits toui/src/**don't show up. - First page load after
setup. The SPA on:1515may show the setup wizard until the agent URL is configured. After that it sticks. - Backend not picking up code changes. Kill and restart Terminal A. Don't restart Vite — it's stateful for HMR.
- The port-swap heuristic only fires when the SPA is served at
:1515. Running Vite on a different port (for example via$env:PORT) skips it; then you needVITE_AGENT_URL. - Don't edit
ui/package.json'sversionfield. It's regenerated fromapp/__version__.pybyscripts/sync_ui_version.py, wired into theprebuild/preweb:buildnpm hooks. See Versioning.