Development
August 27, 2026 · View on GitHub
based is a Bun workspace with four packages:
| Package | What it is |
|---|---|
core/ | Bun server. Every bit of logic and every secret lives here: engine adapters, the agent, the language servers, storage, import/export. REST + NDJSON query streaming + SSE + a WebSocket LSP endpoint on 127.0.0.1:<port>, behind a per-launch bearer token. |
ui/ | React 19 + Vite + Tailwind webview. Talks only to core. |
shell-tauri/ | Tauri 2 native window. Deliberately thin and disposable — it spawns core as a child process and points a window at it. The only shell: dev and release run the same one. |
specs/ | Requirements (specs/based/spec.md) and the tests that verify them. |
Prerequisites
- Bun (a recent 1.x)
- The Rust toolchain (
rustup) — the shell is Rust, sobun run devbuilds it. The firstbun run devon a clean checkout compiles it and takes a few minutes; after that it is near-instant. If you are only working oncoreorui, thedev:core+dev:uibrowser loop below needs no Rust at all. - Windows 11 x64 for the shell and installer.
coreanduiare platform-agnostic enough to develop on, but secrets go through Windows Credential Manager and packaging is Windows-only. - For the installer: Inno Setup 6 (
winget install JRSoftware.InnoSetup) and .NET Framework 4.x (forcsc.exe, which builds the.sqlassociation stub).
bun install
Dev loops
Three ways to run it, fastest feedback first.
bun run dev — the one you want. scripts/dev.ts starts core in watch mode
and Vite, waits for both to listen, then launches the Tauri window pointed at Vite
(BASED_DEV_URL=http://localhost:5183, which makes the shell skip spawning its own core). Full hot
reload inside the real window. Ctrl-C, or closing the window, tears all three down. Logs interleave
in one terminal. Rust changes are not hot-reloaded — restart to rebuild the shell.
bun run dev:core + bun run dev:ui — browser loop. Same core (port 7042, token dev) and
Vite (port 5183, proxying /api), but you iterate at http://localhost:5183 in a browser. The
client falls back to token dev when there's no URL hash, so there's no auth wiring to do.
bun run shell — production-like smoke test. The same Tauri shell with no BASED_DEV_URL, so
it spawns core as a real child process and serves the static ui/dist — the packaged topology, from
the checkout. No watch, no HMR — run bun run build:ui after UI changes or you'll be looking at a
stale bundle.
bun run dev # core + Vite + Tauri window, all with HMR
bun run dev:core # core alone on 127.0.0.1:7042
bun run dev:ui # Vite alone on 5183
bun run build:ui # -> ui/dist
bun run shell # Tauri window + core child over ui/dist
bun run typecheck # core, ui, shell-tauri
bun test # specs
Tests
bun test # from the repo root; runs specs/
Unit tests run anywhere. Integration tests need a real SQL Server and self-skip without one — they will not fail, they will report why they skipped. Point them at a database you control:
$env:BASED_TEST_SERVER = "your-server.database.windows.net"
$env:BASED_TEST_DB = "your_database"
az login
bun test
There is deliberately no default for those variables (specs/based/tests/_devDb.ts):
a real hostname must never be committed, and a silent fallback would make a green run ambiguous
about which database it actually hit. Auth is AzureCliCredential, so az login has to be current.
Most suites only need connect + read. The table-edit suites additionally probe for CREATE TABLE
permission and skip themselves if it isn't there.
The Snowflake suites need no live account — they assert connect-option construction, the bounded connect, and the driver-environment workaround, and run anywhere.
Spec-driven changes
specs/based/spec.md is authoritative. Every requirement has an ID (BASED-*), a test category,
and either an executable test or a written verification procedure. Tests carry a
// Traces: BASED-XXX comment linking back. If you change specified behavior, update the spec in
the same change. See CONTRIBUTING.md.
Building the installer
.\scripts\package-win.ps1 # -> dist\based-<version>-Setup.exe
Five steps: build the UI, bundle the core (shell-tauri/bundle-core.ts →
dist-core/{core,ui,bun}), build the Tauri shell (tauri build --no-bundle; Tauri's own NSIS
bundler is not used — Inno Setup is the installer, and keeping the electrobun-era Inno AppId
means installing over an old install upgrades it in place instead of registering a second
"based"), stage based-shell.exe + resources + icon, and run Inno Setup. The version comes from
shell-tauri/tauri.conf.json, which is the single source of
truth. Requires Inno Setup 6 (winget install JRSoftware.InnoSetup) and the Rust toolchain.
Building for macOS
Actions tab -> "build (macOS)" -> Run workflow
macOS apps cannot be cross-compiled from Windows: the macOS SDK is licensed to Apple hardware, and
tauri build shells out to hdiutil and iconutil to make the bundle. So the build runs on a
GitHub-hosted macOS runner — real Mac hardware — via
.github/workflows/build-macos.yml, and no Mac is needed to
produce a .dmg. Standard runners are free for public repositories.
Same first three steps as Windows (build:ui → bundle-core.ts → tauri build), but it bundles a
.dmg rather than handing an unbundled exe to Inno Setup, and it checks the bundle layout first
(libduckdb.dylib present, bun/bun present and executable) so a misbuild fails at the runner
instead of on a user's machine.
The port work this workflow existed to de-risk has landed: dialogs are relayed to the shell
(/api/shell/dialogs), the macOS menu/lifecycle/file-open conventions are implemented, and
shortcuts are platform-correct. What remains is verification on real hardware — Phase 7 of
specs/based/plans/macos-port.md.
Cutting a release
.\scripts\release.ps1 patch # or minor / major / -Version 1.0.0
.\scripts\release.ps1 patch -DryRun # bump + changelog draft only, publish nothing
The local half: preflight (clean tree, on main) → bump → draft a CHANGELOG.md section from the
commit log and stop for you to rewrite it → commit, tag, push. The tag push triggers
release.yml, which typechecks, tests, builds the Windows
installer (Inno Setup via choco) and the macOS .dmg on pinned runners, publishes one GitHub
release with both artifacts + SHA-256s + per-platform install notes, and bumps the
Cyronius/homebrew-based cask (needs the TAP_PUSH_TOKEN secret; skips with a warning if absent).
Version bumping alone:
.\scripts\bump-version.ps1 patch -WhatIf # print the transition, write nothing
.\scripts\bump-version.ps1 minor
It rewrites the version in shell-tauri/tauri.conf.json and shell-tauri/Cargo.toml (kept in
step — Cargo.toml feeds the exe's file-version metadata) and regenerates
core/src/version.ts, which is committed so a fresh clone typechecks
without running the script first. The version reaches the status bar via /api/health.
Builds happen in CI, not locally. release.ps1 used to build the installer on this machine
(Inno Setup + Rust toolchain required locally); Phase 6 of the
macOS port plan split it at the build boundary, so releases
are reproducible from the repository alone and both platforms build from the same tag.
Note on PowerShell scripts
Keep scripts/*.ps1 ASCII-only. Windows PowerShell 5.1 decodes .ps1 files as ANSI, so a
UTF-8 em-dash or curly quote — even inside a comment — corrupts the parse of the entire script.