This guide covers local development setup for Mesh Client (Meshtastic, MeshCore, and Reticulum), including cloning, prerequisites, and test harness tooling. For runtime errors, connection issues, and packaged-app problems, see troubleshooting.md.
These requirements apply to all platforms.
- Git
- Node.js 22.13.0+ and pnpm 11+ (
package.jsonengines; the repo pinspackageManagerto a specific pnpm release — use Corepack when available, ornpm install -g corepack@latest/npm install -g pnpm@<pin>on Node 25+ where Corepack is not bundled).pnpm installfails on engine mismatch. After pulling a pnpm major bump,preinstallandpnpm run devprint an upgrade banner with the exact install command if your local pnpm is too old or the wrong major. - CI uses Node 22
- Python 3 +
pip(needed for MkDocs documentation build and yamllint)
Verify:
git --version
node --version
pnpm --versionRun the automated checklist after cloning (works before pnpm is installed):
node scripts/check-environment.mjs # before pnpm install
pnpm install
pnpm run check:environment # re-check after install
pnpm run devRequired checks (must pass): Git, Node.js, pnpm, node_modules, and platform-native build tools (Xcode CLT on macOS, g++/make on Linux, MSVC cl on Windows).
Optional checks (warnings only): Python/pip, Rust, actionlint, yamllint, a Docker-compatible container engine (Podman preferred), act, and Linux dialout group membership. For local CI you can use container mode (pnpm run act:ci — act + Podman/Docker) or host mode (pnpm run act:ci:native — no container engine). Fix optional items when you need docs builds, pre-commit hooks, Reticulum sidecar work, local CI parity, or USB serial on Linux.
Use the printed → hints and setup:* scripts (setup:build-deps, setup:actionlint, setup:dialout) to fix failures. See Helper scripts (auto-install where possible) below.
Docs are built with MkDocs Material.
- Create and activate a local virtual environment (recommended on macOS/Homebrew Python because of PEP 668 externally managed environments):
- macOS/Linux:
python3 -m venv .venvsource .venv/bin/activate
- Windows PowerShell:
py -3 -m venv .venv.\.venv\Scripts\Activate.ps1
- macOS/Linux:
- Install the docs dependencies:
pnpm run docs:install- or (manual):
python3 -m pip install -r docs/requirements.txt
- Build locally:
pnpm run docs:build
- Preview locally:
pnpm run docs:serve
If pnpm run docs:install fails with externally-managed-environment, activate .venv and rerun.
git clone https://github.com/Colorado-Mesh/mesh-client
cd mesh-client
node scripts/check-environment.mjs # optional but recommended on first clone
pnpm install
pnpm run check:environment # re-check after installIf you are updating from an older clone, use a clean install when troubleshooting native module issues:
rm -rf node_modules package-lock.json
pnpm install- Dev mode (hot reload):
pnpm run dev - Production-like local start:
pnpm start
Reticulum/LXMF runs in a separate Rust binary (mesh-client-reticulum) spawned by the Electron main process. The MIT TypeScript layers talk to it over localhost HTTP/WS only. You only need this when working on the Reticulum protocol tab.
Recommended: rustup — matches CI and pnpm run update:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env" # or open a new terminal
rustc --version
cargo --versionmacOS Homebrew alternative: brew install rust works for local cargo build, but CI uses rustup. Do not install both rustup and Homebrew rust on the same machine — they can fight over rustc/cargo on your PATH. Pick one:
| Approach | Install | Update |
|---|---|---|
| rustup (preferred) | rustup.rs | rustup update (also run by pnpm run update) |
| Homebrew | brew install rust |
brew upgrade rust |
If you use Homebrew only, pnpm run update will try brew upgrade rust when rustup is absent. For cross-target builds (WoA, Linux glibc) and toolchain pins, prefer rustup.
Linux and Windows: use rustup; on Windows you may also need Visual Studio Build Tools with the C++ workload (same as native Node modules).
reticulum-sidecar/rust-toolchain.toml pins stable and installs clippy, rustfmt, and llvm-tools-preview on the first cargo command run inside reticulum-sidecar/ (rustup auto-install). When cargo is on your PATH and sidecar-related paths are staged, pre-commit runs pnpm run check:reticulum-sidecar (full-feature fmt + Clippy + test with rns-stack,rns-ble,rns-rnode-tcp — no coverage).
From the repo root:
pnpm run reticulum:sidecar:buildThis writes reticulum-sidecar/target/debug/mesh-client-reticulum (macOS/Linux) or .exe on Windows.
First-time / recover the stack workspace: from the mesh-client repo root, run ./scripts/clone-ratspeak-stack.sh. That script clones (or updates) the repo-local .rsstack/ workspace checkouts rsReticulum, rsLXMF, rsNomad, rsLXST, and lrgp-rs, floats each to origin/main by default, and applies mesh-client overlays (fails if a patch will not apply). For bisect or a known-good pin, set RS_RETICULUM_REF / RS_LXMF_REF / RS_NOMAD_REF / RS_LXST_REF / RS_LRGP_REF to a SHA or ref before running the clone script.
When those .rsstack/ checkouts already exist, pnpm run reticulum:sidecar:build applies required overlays via scripts/ensure-rsReticulum-patches.sh before compiling with rns-stack,rns-ble,rns-rnode-tcp. See reticulum-sidecar/patches/README.md for overlay details.
First run in Electron dev: Reticulum → Connection → Start stack will run cargo build automatically if that binary is missing (first compile can take a few minutes). Pre-build with the command above to avoid waiting on the first click.
# Optional: run sidecar standalone (port 19437)
pnpm run reticulum:sidecar:dev
# Health check (standalone or after Start stack in the app)
curl -s http://127.0.0.1:19437/api/v1/status
# → {"status":"ok",...}In Electron dev: open the Reticulum protocol pill (amber) → Connection → Start stack. Then use Network for identity (generate/import) and Connection for interfaces (add, edit, delete). Dev builds resolve the binary from reticulum-sidecar/target/debug/mesh-client-reticulum.
pnpm run update updates Node dependencies and, when cargo is available:
- Runs
rustup update(orbrew upgrade rustif you use Homebrew rust without rustup) - Rebuilds the sidecar with
cargo buildinreticulum-sidecar/
Scope: pnpm update / pnpm-lock.yaml changes are repo-local (commit the lockfile on your branch). The sidecar rebuild writes only to gitignored reticulum-sidecar/target/. Rust toolchain updates are not repo-scoped — rustup update refreshes the toolchain in your user profile (~/.rustup, ~/.cargo/bin), shared by any Rust project on the machine. The committed rust-toolchain.toml selects stable and required components for this crate; rustup applies it when you build or lint inside reticulum-sidecar/.
By default, pnpm run update keeps reticulum-sidecar/target/ so the next Electron Start stack or reticulum:sidecar:build stays warm. After a verify-and-walk-away run (or when reclaiming disk — often ~2–3G), opt in to a full cargo clean after a successful rebuild:
# Preferred
CLEAN_SIDECAR_TARGET=1 pnpm run update
# Equivalent flag (pnpm forwards args after --)
pnpm run update -- --clean-target
# Ad hoc anytime (does not run update)
pnpm run reticulum:sidecar:cleanSkip cleanup while iterating on sidecar Rust or Reticulum in Electron — the next build will be cold (several minutes).
| Command | When |
|---|---|
pnpm run check:reticulum-sidecar |
Pre-commit full-feature fmt + Clippy + test (when cargo on PATH) |
pnpm run reticulum:sidecar:clippy:full |
Before PR when editing reticulum-sidecar/** |
pnpm run reticulum:sidecar:coverage |
Optional local HTML report (cargo install cargo-llvm-cov) |
CI: full-feature lint in reticulum-sidecar.yaml; line-coverage threshold in tests.yaml when sidecar paths change (not pre-commit).
- Optional full RNS stack:
reticulum-sidecar/README.md(rns-stackCargo feature) - Architecture: docs/reticulum.md
- HTTP contract: docs/reticulum-sidecar-ipc.md
- Won't start / health timeout: troubleshooting.md#reticulum-sidecar-wont-start-or-health-poll-times-out
Windows ARM64 release builds use a dedicated aarch64-pc-windows-msvc CI job (see .github/workflows/reticulum-sidecar.yaml).
Use these from the repository root:
# App run/build
pnpm run dev
pnpm start
pnpm run build
# Platform packaging (binary artifacts in release/)
pnpm run dist:mac
pnpm run dist:linux
pnpm run dist:win
# Quality checks
pnpm run check:environment
pnpm run test:run
pnpm run test:coverage
pnpm run lint
pnpm run typecheck
pnpm run format:check
pnpm run check:i18n
# Maintenance
pnpm run update
pnpm run clean
pnpm run clean:build
pnpm run clean:build:full
# Reticulum sidecar (optional; requires Rust)
pnpm run reticulum:sidecar:build
pnpm run reticulum:sidecar:dev
pnpm run check:reticulum-sidecar
pnpm run reticulum:sidecar:clippy:full
pnpm run reticulum:sidecar:coverage
# Docs
pnpm run docs:install
pnpm run docs:build
pnpm run docs:serveBoth clean:build and clean:build:full (cross-platform; see scripts/clean-build.mjs) print the full list of what will be removed and always confirm with a Proceed? [y/N] prompt before deleting anything (default N; pass -y/--yes to skip, in which case the plan is still printed). They never run when stdin is not a TTY and -y is absent, so they cannot hang in CI.
clean:build(shallow) — removes build dists, test output, and caches (dist,dist-electron,release,coverage,.vitest-reports,test-results,playwright-report,.eslintcache). It keepsnode_modules/and the Reticulum sidecar, sopnpm run dev/pnpm startand the sidecar keep working. Use it to force fresh rebuilds, clear stale or corrupt intermediate output, or free modest disk without re-installing.clean:build:full— removes everything above plusnode_modules,reticulum-sidecar/target/, and the bundled sidecar binary (resources/reticulum-sidecar/), then runspnpm installandpnpm run reticulum:sidecar:buildso the environment is left in a working state. Use it to fix a corrupt/driftednode_modules, clear stale Rust incremental state after toolchain or overlay-patch changes, or reclaim up to ~20G+ of disk while restoring a known-good environment. The reinstall is skipped if nothing in tier 2 was actually present.
clean:build and clean:build:full are the cross-platform equivalents of make clean/deep clean; the repo's older Unix-only clean (clean above) remains for its narrower, rm -rf-style behaviour.
Complete reference of all pnpm scripts in package.json, organized by category.
| Script | Description |
|---|---|
build |
Full production build: main (minified) + preload + renderer |
build:main |
Build main process (no minify) → dist-electron/main/index.js |
build:main:prod |
Build main process (minified) → dist-electron/main/index.js |
build:main:meta |
Build main with metadata JSON (no minify) → dist-electron/main/metafile.json |
build:main:minify-meta |
Build main with metadata JSON (minified) → dist-electron/main/meta.json |
build:main:size |
Print main bundle size |
build:preload |
Build preload script → dist-electron/preload/index.js |
build:renderer |
Build renderer (React app) via Vite → dist/ |
| Script | Description |
|---|---|
dev |
Hot-reload dev mode: main/preload watch + Vite dev server + Electron |
start |
Production-like local start: build then Electron with security warnings enabled |
electron:open |
Launch Electron (requires prior build) |
trace-deprecation |
Run with Node --trace-deprecation enabled |
| Script | Description |
|---|---|
dist |
Build for current platform → release/ |
dist:mac |
Build macOS .dmg + .zip + verify packaging (verify-mac-packaging.mjs) |
dist:mac:publish |
Build macOS and upload to release server |
dist:linux |
Build Linux x64 + arm64 (.AppImage, .deb, .rpm) + verify packaging |
dist:linux:publish |
Build Linux and upload to release server |
dist:win |
Build Windows .exe installer (hoisted install workaround) + verify packaging |
dist:win:publish |
Build Windows and upload to release server |
dist:mac, dist:linux, and predist run dedupe:dist (scripts/dedupe-dist.mjs) before packaging; that helper retries on transient @jsr/_tmp_* rename races. dist:win uses scripts/dist-win-hoisted-install.mjs and restores node_modules afterward.
Flatpak builds use flatpak-builder directly (not a pnpm script) and require a one-time local setup. The GitHub Actions workflow (flatpak.yaml) handles this in CI automatically; the steps below are for local iteration.
1. Install system tools (Debian/Ubuntu)
sudo apt install flatpak flatpak-builder elfutils1. Install system tools (Fedora)
sudo dnf install flatpak flatpak-builder elfutils2. Add Flathub and install runtimes (one-time, ~500 MB)
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak install --user -y flathub org.freedesktop.Platform//24.08
flatpak install --user -y flathub org.freedesktop.Sdk//24.08
flatpak install --user -y flathub org.freedesktop.Sdk.Extension.node22//24.08
flatpak install --user -y flathub org.electronjs.Electron2.BaseApp//24.083. Generate offline pnpm sources (re-run whenever pnpm-lock.yaml changes)
# Prefer the CI pin with --force-reinstall (images may preinstall an older 0.1.0).
# See scripts/flatpakPnpmStoreVersion.mjs FLATPAK_NODE_GENERATOR_GIT.
pip install --force-reinstall --no-cache-dir \
"git+https://github.com/flatpak/flatpak-builder-tools@ac5a296ac6111aa2319daf532f609a067b88d8a9#subdirectory=node"
# Must match package.json packageManager major (pnpm 11 → v11). Generator defaults to v10.
PNPM_MAJOR="$(node -p "require('./package.json').packageManager.match(/^pnpm@(\\d+)/)[1]")"
STORE_VERSION="v${PNPM_MAJOR}"
flatpak-node-generator pnpm pnpm-lock.yaml \
--pnpm-store-version "$STORE_VERSION" \
-o flatpak/generated-sources.jsonflatpak/generated-sources.json is generated automatically in the flatpak.yaml CI workflow and does not need to be committed. For local builds you generate it manually as shown above; the file is only required locally and in a Flathub submission repo.
PR-cycle offline check (not pre-commit): after installing flatpak-node-generator, run pnpm run check:flatpak-offline-pnpm to assert the workflow passes the correct --pnpm-store-version and that generated sources cover lockfile tarballs (catches ERR_PNPM_NO_OFFLINE_TARBALL). CI / act:pr / release.sh run this automatically.
Offline install inside the Flatpak sandbox uses scripts/flatpak-pnpm-install.mjs, which retries transient @jsr/_tmp_* rename races (same root cause as Windows dist:win hoisted installs).
3b. Bundle Reticulum sidecar (required for Reticulum tab in the Flatpak; CI does this automatically)
The manifest copies resources/ into the app, including resources/reticulum-sidecar/mesh-client-reticulum. Before flatpak-builder, build the sidecar and install the binary there:
pnpm run reticulum:sidecar:build
install -Dm755 reticulum-sidecar/target/debug/mesh-client-reticulum \
resources/reticulum-sidecar/mesh-client-reticulumFor a release-quality local Flatpak, use cargo build --release with rns-stack,rns-ble,rns-rnode-tcp (see Reticulum sidecar).
4. Build and install locally
flatpak-builder --user --install --force-clean build-dir org.coloradomesh.MeshClient.ymlThis installs the app into your user Flatpak store.
5. Run
flatpak run org.coloradomesh.MeshClient6. Produce a .flatpak bundle (for sharing without a repo)
flatpak build-bundle ~/.local/share/flatpak/repo \
org.coloradomesh.MeshClient.flatpak \
org.coloradomesh.MeshClient stableInstalling a .flatpak file creates a one-off remote named like meshclient-origin (not flathub); that is expected. The ref branch is stable (release CI sets this; older artifacts used master). Version is shown in MetaInfo / flatpak info, not in the remote name.
Reinstall after downloading a new bundle
flatpak uninstall --user org.coloradomesh.MeshClient
flatpak install --user ./org.coloradomesh.MeshClient-aarch64.flatpak
flatpak run org.coloradomesh.MeshClientRuntime issues (GPU, VMware guests): see Flatpak: vmwgfx: driver missing (VMware on macOS).
Launch failures on Arch/CachyOS Wayland: see Flatpak: immediate exit on Arch / CachyOS / Wayland (#598).
Lint the manifest before submitting to Flathub:
flatpak run --command=flatpak-builder-lint org.freedesktop.Sdk \
manifest org.coloradomesh.MeshClient.yml| Script | Description |
|---|---|
test |
Run tests in watch mode (Vitest) |
test:run |
Run tests once (CI mode) |
test:coverage |
Run tests once with V8 coverage report |
test:verbose |
Run tests with verbose output |
| Script | Description |
|---|---|
lint |
Run ESLint (type-aware, zero warnings) |
lint:fix |
Run ESLint with auto-fix |
lint:md |
Run markdownlint-cli2 on all .md files |
format |
Format all code via Prettier + sort package.json |
format:check |
Check formatting without fixing |
| Script | Description |
|---|---|
typecheck |
TypeScript check: renderer + main process |
typecheck:strict-shared |
Strict TypeScript (noUncheckedIndexedAccess + exactOptionalPropertyTypes) for src/shared |
check:pr |
PR-parity local gate: lint + typecheck + strict-shared + full test:run (+ sidecar if branch touches it) |
| Script | Description |
|---|---|
check:codeql-extensions |
Verify CodeQL extension allowlist for custom queries |
check:console-log |
Fail on bare console.log in production paths |
check:db-migrations |
Verify SQLite migrations are valid |
check:electron-security |
Verify Electron security settings (CSP, sandbox, etc.) |
check:environment |
Verify local dev prerequisites (run after clone) |
check:flatpak |
Lint Flatpak manifest and wrapper scripts |
check:flatpak-offline-pnpm |
PR/release offline Flatpak pnpm store vN + lockfile coverage |
check:i18n |
Verify English keys, unused keys, and locale quality rules |
check:i18n:branch |
Run i18n quality checks on keys new/changed vs HEAD only |
check:insecure-temp-files |
Predictable os.tmpdir() writes (CodeQL js/insecure-temporary-file) |
check:ipc-contract |
Verify IPC channel contracts between main/preload/renderer |
check:licenses |
Summarize dependency licenses (license-checker-rseidelsohn) |
check:log-injection |
Detect unsanitized user data in log calls |
check:log-panel-filter |
Verify log panel filter wiring |
check:log-service-sinks |
Verify log service sink configuration |
check:protocol-string-gates |
Enforce protocol capability gates over string compares |
check:reticulum-decommissioned-hubs |
Keep TS/Rust decommissioned hub lists aligned |
check:reticulum-interface-modes |
Keep TS/Rust Reticulum interface-mode catalogs aligned |
check:reticulum-sidecar |
Full-feature cargo fmt + Clippy + test (skips when cargo missing) |
check:silent-catches |
Detect empty or unlogged catch blocks |
check:url-hostname-sanitization |
Verify URL hostname sanitization helpers |
check:xss-patterns |
Detect risky DOM/HTML sink patterns |
| Script | Description |
|---|---|
docs:install |
Install MkDocs Python dependencies |
docs:build |
Build static docs to site/ |
docs:serve |
Serve docs locally with live reload |
| Script | Description |
|---|---|
act:ci |
Run Linux CI workflow via act + containers (Podman preferred) |
act:ci:native |
Run CI checks on the host (no container engine) |
act:tests |
Run tests workflow via act + containers |
act:tests:native |
Run test:coverage on the host |
act:pr |
Run act:ci then act:tests (container) |
act:pr:native |
Run native CI + tests on the host |
act:build:linux |
Run build.yaml ubuntu leg via act + containers |
act:build:linux:native |
Run dist:linux on the host |
act:reticulum |
Reticulum sidecar Linux jobs via act |
act:reticulum:native |
Reticulum sidecar stub cargo test / build on the host |
act:flatpak |
Flatpak x86_64 workflow via act (slow; privileged) |
act:pull-images |
Pre-pull Docker images for act (container mode) |
act:list |
List container and native act targets |
| Script | Description |
|---|---|
clean |
Remove dist-electron, dist, and node_modules |
clean:build |
Remove build dists, test output, and caches (keeps node_modules + sidecar); prompts [y/N] (default N) |
clean:build:full |
Also remove node_modules + Reticulum sidecar build output, then reinstall deps + rebuild sidecar; prompts [y/N] (default N) |
dedupe:dist |
Retrying dist dedupe (scripts/dedupe-dist.mjs; @jsr/_tmp_* races) |
i18n:auto-translate |
Machine-translate missing locale keys (MyMemory default) |
i18n:prune-unused |
Remove orphaned translation keys from locale files |
rebuild |
Rebuild native Node modules for Electron |
release |
Maintainer release script (scripts/release.sh) |
reticulum:sidecar:build |
Build debug mesh-client-reticulum (requires cargo) |
reticulum:sidecar:clippy |
Clippy stub build (-D warnings) |
reticulum:sidecar:clippy:full |
Clippy with rns-stack,rns-ble,rns-rnode-tcp |
reticulum:sidecar:coverage |
Optional HTML coverage via cargo llvm-cov (no CI threshold) |
reticulum:sidecar:dev |
Run sidecar standalone on 127.0.0.1:19437 |
reticulum:sidecar:fmt |
cargo fmt in reticulum-sidecar/ |
reticulum:sidecar:fmt:check |
cargo fmt --check |
reticulum:sidecar:test |
Full-feature cargo test (clones the .rsstack/ workspace if needed) |
reticulum:sidecar:test:full |
Alias for reticulum:sidecar:test |
setup:actionlint |
Install actionlint for GitHub workflow linting |
setup:build-deps |
Install native build dependencies |
setup:dialout |
Add user to dialout group for serial port access (Linux) |
update |
Update pnpm deps, Rust toolchain (rustup), rebuild sidecar |
| Script | Description |
|---|---|
preinstall |
Require pnpm 11+ (check-package-manager.mjs) then only-allow pnpm |
postinstall |
Rebuild native Node modules for Electron + apply pnpm patches |
prepare |
Enable git hooks (core.hooksPath = .githooks) |
predist |
Run dedupe:dist before dist packaging |
postinstall runs scripts/rebuild-native.mjs for Electron native addons and applies patchedDependencies from pnpm-workspace.yaml (Meshtastic JSR transports, MeshCore, readable-stream, usb, etc.). When bumping patched packages, update hashes under patches/ and keep WATCH_ENTRIES in scripts/update.sh in sync — see AGENTS.md.
Automated dependency updates are configured in .github/dependabot.yml:
- Schedule: Weekly on Saturdays
- pnpm dependencies: Grouped PRs;
electronseparate, all other deps together - GitHub Actions: Grouped into one PR
Testing Dependabot PRs locally:
Always use pnpm to test dependabot PRs:
git checkout <dependabot-branch>
pnpm install --frozen-lockfile
pnpm run build
pnpm run test:runDo not use npm install; it creates a package-lock.json and may not respect pnpm's lockfile format.
This section is the project test harness setup.
Installed via pnpm install (from package.json):
vitest,@vitest/coverage-v8, and renderer/main test dependencies@playwright/test(Electron E2E; browser downloads skipped viaPLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1in.npmrc)eslint,typescript,typescript-eslintprettier,prettier-plugin-sh,prettier-plugin-tailwindcssmarkdownlint-cli2,vitest-axe
Not installed by pnpm (install separately when needed):
actionlint(recommended for workflow linting; runpnpm run setup:actionlintor install system-wide)yamllint(required for YAML linting; install viapip install yamllintorbrew install yamllinton macOS)- Rust /
cargo(optional; Reticulum sidecar — see Reticulum sidecar; prefer rustup).rust-toolchain.tomlinstalls clippy/rustfmt/llvm-tools-preview on first build inreticulum-sidecar/. Optional:cargo install cargo-llvm-covforpnpm run reticulum:sidecar:coverage. - Podman or Docker (Docker-compatible engine) and
act(optional for container CI —act:*; host CI usesact:*:nativewithout a container engine) - Python 3 +
venv+ MkDocs Python deps (for docs checks/builds)
vitest.config.mts defines three projects:
| Project | Environment | Role |
|---|---|---|
renderer-ui |
jsdom | Component/hook tests with setup stubs |
renderer-logic |
node | Pure renderer unit tests (no setup) |
main |
node | Main, shared, preload, and script tests |
Worker counts are derived in vitest.harness.mts via computeVitestMaxWorkers(cpuCount, ratio): jsdom workers use RENDERER_UI_CPU_RATIO because they are memory-heavy; node workers use NODE_WORKER_CPU_RATIO. Both pools floor at MIN_VITEST_WORKERS (currently 2) and cap effective CPU count at MAX_VITEST_CPU_COUNT (32). When tuning worker allocation, change those constants in the harness (not this doc). Shared Vite dependency inline lists (VITEST_CORE_DEPS, VITEST_SERVER_INLINE_DEPS) also live there — add new deps to the harness when tests need them inlined.
By default all three Vitest projects run in parallel (groupOrder: 0). On memory-constrained hosts, set VITEST_SEQUENTIAL_PROJECTS=1 to run renderer-ui first, then renderer-logic + main together (legacy behavior).
CI runs coverage in three parallel jobs (renderer-ui, renderer-logic, main) and merges blob reports via pnpm run test:coverage:merge (see .github/workflows/tests.yaml).
Standalone suite under e2e/ launches the unpackaged production build with Playwright’s Electron API (playwright._electron.launch), using resolveLocalElectronBin() and an isolated --user-data-dir (mesh-e2e-* under the OS temp dir). Specs cover startup, preload IPC, tabs, window lifecycle, protocol switch, settings relaunch, empty states, reload, locale, diagnostics shell, and save-dialog cancel — not BLE/serial/MQTT/hardware or Reticulum sidecar flows.
| Command | Purpose |
|---|---|
pnpm run test:e2e:build |
pnpm run build then Playwright |
pnpm run test:e2e |
Playwright only (requires existing dist-electron + renderer build) |
pnpm run test:e2e:headed |
Headed Electron windows |
Constraints:
workers: 1— main usesrequestSingleInstanceLock(); parallel launches flake.- Linux: set
MESH_CLIENT_DISABLE_GPU=1(harness does this); CI usesxvfb-run -a. Local headless Linux needsDISPLAYfor directpnpm run test:e2e, or wrap withxvfb-run -a(pnpm run check:environmentwarns whenDISPLAYis unset). - Not wired into pre-commit,
pnpm run test:run, orpnpm run check:pr. Daily + manual CI:.github/workflows/e2e.yaml.
Monolithic protocol runtimes (useMeshtasticRuntime, useMeshcoreRuntime) also use source contract tests (read .ts files and assert wiring strings) where full renderHook integration would require heavy BLE/MQTT mocking; see *.reconnect*.test.ts beside those runtimes. Another example: meshtasticRuntimeWireEffects.diagnostics.contract.test.ts asserts LocalStats / RF hop-SNR still call processNodeUpdate from meshtasticNodeSideEffects / meshtasticRawPacketSideEffects.
When you open the Vite dev URL in a plain browser tab (not the Electron window), installDevElectronApiStubIfNeeded() in src/renderer/main.tsx installs a no-op window.electronAPI stub (devElectronApiStub.ts). UI shell and most panels render, but RF connect, SQLite, MQTT IPC, and file dialogs require the Electron window. The console logs [dev] Installed browser electronAPI stub… when the stub is active.
Run these quality checks before opening a PR:
pnpm run test:run
pnpm run lint
pnpm run lint:md
pnpm run typecheck
pnpm run format:check
pnpm run check:i18n
# Reticulum sidecar (when editing reticulum-sidecar/**)
pnpm run reticulum:sidecar:clippy:full
pnpm run reticulum:sidecar:testOther useful commands:
pnpm test(watch mode — reruns only changed test files)pnpm run test:staged(pre-commit helper: Vitest related to staged files only)pnpm run test:changed(one-shot tests for working-tree edits vsHEAD, including unstaged WIP)pnpm run check:pr(before opening/updating a PR: full lint + typecheck +typecheck:strict-shared+ fulltest:run)pnpm run test:ui/test:logic/test:main(single Vitest project)pnpm run test:coverage(CI coverage report; used byact:tests:native)pnpm run test:coverage:merge(merge sharded CI blob reports locally)pnpm run reticulum:sidecar:coverage(optional local HTML; CI threshold intests.yaml)pnpm run test:verbose(verbose failures)pnpm run check:i18n:branch(i18n quality on branch-diff keys only)pnpm run i18n:auto-translate(fill missing keys)pnpm run i18n:prune-unused -- --write(drop orphaned locale keys)
The pre-commit hook runs path-gated check:* steps plus staged-related Vitest — see Git hooks.
Use the platform-specific packaging command:
pnpm run dist:mac # macOS -> .dmg + .zip in release/
pnpm run dist:linux # Linux -> .AppImage + .deb in release/
pnpm run dist:win # Windows -> .exe installer in release/Output goes to the release/ directory.
macOS (dist:mac) runs electron-builder --mac --publish never, then node scripts/verify-mac-packaging.mjs. The verify step asserts .dmg + .zip artifacts, symlink-preserving ZIP extract (ditto -xk), DMG mount, launcher/framework sizes, and bundled Reticulum sidecar — same checks CI packaging-smoke uses on downloaded artifacts. It does not require signing secrets; unsigned local builds are expected to pass verify.
Optional macOS signing (release parity): export the same env vars CI uses before pnpm run dist:mac or dist:mac:publish:
export CSC_LINK='…' # base64 .p12 Developer ID Application cert
export CSC_KEY_PASSWORD='…'
export CSC_IDENTITY_AUTO_DISCOVERY=true
export APPLE_ID='…'
export APPLE_APP_SPECIFIC_PASSWORD='…'
export APPLE_TEAM_ID='…'
pnpm run dist:macWhen CSC_LINK is unset, electron-builder skips signing/notarization (CSC_IDENTITY_AUTO_DISCOVERY=false in CI). See Release Process — macOS code signing and notarization.
To analyze the main process bundle size and composition:
pnpm run build:main:minify-metaThis generates dist-electron/main/meta.json. Upload this file to esbuild's online analyzer to visualize:
- Bundle size by dependency
- Code that could be externalized
- Minification effectiveness
After pnpm install, repo hooks are enabled via core.hooksPath (see the prepare script in package.json). The pre-commit hook runs on every commit. Typical commits run staged-related Vitest only (pnpm run test:staged → vitest related on staged source/test files, optionally narrowed to matching Vitest projects). Unstaged WIP is ignored. Full typecheck still runs every commit; ESLint runs on staged JS/TS with --cache (CI still runs full-tree lint). Path-gated typecheck:strict-shared runs when src/shared/ (or tsconfig.strict.json) is staged. Several expensive check:* steps and pnpm audit / full-feature sidecar builds are path-gated.
ESLint: production src/** enforces no-unsafe-*; test files keep those off. no-unnecessary-condition is enforced for src/shared/** and src/renderer/lib/** only.
Green pre-commit does not replace PR CI: .github/workflows/tests.yaml always runs the full Vitest suite with coverage. Use pnpm run check:pr before opening a PR.
Hook order (authoritative source: .githooks/pre-commit):
- If
package.jsonorpnpm-lock.yamlis staged:pnpm install --frozen-lockfile - Prettier on staged files only (not whole-tree
pnpm run formatunless you run it manually) - markdownlint-cli2 on staged
.mdfiles only (not fullpnpm run lint:mdunless you run it manually) - When
package.jsonorpnpm-lock.yamlis staged:pnpm dedupe, re-stagepnpm-lock.yaml, then re-stage the originally staged paths - When
src/renderer/locales/en/translation.jsonis staged:pnpm run i18n:auto-translate(incremental vsHEADEnglish, not--all) and re-stagesrc/renderer/locales/— see Internationalization - ESLint on staged JS/TS with
--cache(skip when none staged) pnpm run typecheck(full tree); path-gatedtypecheck:strict-sharedwhensrc/shared/ortsconfig.strict.jsonstaged- Always-on:
check:electron-security,check:log-injection,check:log-service-sinks,check:codeql-extensions,check:insecure-temp-files,check:console-log,check:silent-catches,check:url-hostname-sanitization,check:xss-patterns,check:protocol-string-gates,check:log-panel-filter,check:licenses;check:i18nwhen English locale staged elsecheck:i18n:branch - Path-gated:
check:flatpak,check:db-migrations,check:ipc-contract,check:reticulum-interface-modes,check:reticulum-decommissioned-hubs,check:reticulum-sidecar(whencargoonPATHand sidecar paths staged) pnpm audit --audit-level=highonly when dependency manifests staged;actionlintwhen.github/workflows/*staged;yamllintwhen any*.yaml/*.ymlstagedpnpm run test:staged(scripts/precommit-tests.mjs: staged-onlyvitest related; full suite when vitest config/setup mocks or dependency manifests change; skip when no source/test staged)
Release / CI full suite: pnpm run release (scripts/release.sh) and PR tests.yaml always run pnpm run test:run (full Vitest) — never test:staged. Release also runs the ungated check:* set and requires actionlint + yamllint. Use pnpm run check:pr for the same Vitest/lint/typecheck surface locally before a PR.
Install hook dependencies via Helper scripts (setup:actionlint, yamllint via pip/brew/apt).
Emergency bypass (temporary only):
git commit --no-verifyRun any skipped checks manually as soon as possible.
Local CI has two modes:
| Mode | Scripts | Requires |
|---|---|---|
| Container | pnpm run act:ci, pnpm run act:tests, … |
Docker-compatible engine + act (Podman preferred) |
| Host / native | pnpm run act:ci:native, pnpm run act:tests:native, … |
Node/pnpm only |
Container mode runs GitHub Actions jobs inside containers using a Docker-compatible engine (Podman Desktop preferred). Host mode runs the same pnpm/cargo steps directly — use this when no container engine is available or act cannot reach the daemon.
Install (container mode):
| OS | Container engine | act |
|---|---|---|
| macOS | Podman Desktop with Docker compatibility enabled | brew install act |
| Linux | Podman or Docker engine | act releases |
| Windows | Podman Desktop with Docker compatibility enabled | choco install act-cli or releases |
pnpm run act:pull-images # container mode only
pnpm run act:list
pnpm run act:ci # or act:ci:native
pnpm run act:tests # or act:tests:nativeOn macOS, podman exposes a Docker-compatible socket at /var/run/docker.sock; pass that path to act via ACT_DOCKER_SOCKET, or let it auto-detect if Podman created the socket symlink. If you use Docker Desktop instead of Podman, its socket is typically under ~/.docker/run/docker.sock.
macOS and Windows installer builds still require native pnpm run dist:mac / pnpm run dist:win. See ci-cd.md.
- actionlint: required for local pre-commit if workflow files are touched.
These scripts try to install optional tooling automatically. If they fail (for example, missing sudo/admin rights), follow the manual steps in this doc instead.
- Verify your environment (recommended after a fresh clone):
node scripts/check-environment.mjs(beforepnpm install)pnpm run check:environment(afterpnpm install)
- Install
actionlint(used by the git pre-commit hook):pnpm run setup:actionlint- This installs into
.githooks/binso the hook can find it.
- Install
yamllint(required by the git pre-commit hook):- Install manually via pip:
pip install yamllint - macOS alternative:
brew install yamllint - Linux alternative:
sudo apt install yamllint(Debian/Ubuntu) orsudo dnf install yamllint(Fedora)
- Install manually via pip:
- Install native build dependencies:
pnpm run setup:build-deps- Linux/macOS: attempts to install what native builds need (requires sudo where applicable).
- Windows: prints a message to install Visual Studio Build Tools manually.
- (Linux only) Fix serial port permissions:
pnpm run setup:dialout- Adds your user to the
dialoutgroup (requires sudo + re-login).
The app uses i18next for localization. English is the source of truth.
- Locale files:
src/renderer/locales/{en,es,uk,de,zh,pt-BR,fr,it,pl,cs,ja,ru,nl,ko,tr,id}/translation.json - Adding strings:
- Add the new key and English value to
src/renderer/locales/en/translation.json. - Use the
t('key.name')hook in React components. - Run
pnpm run i18n:auto-translateto machine-translate the new key into other supported languages. - Run
pnpm run check:i18nto verify all keys are valid and accounted for.
- Add the new key and English value to
- Removing strings: delete the English key, then run
pnpm run i18n:prune-unused -- --write(or remove manually from every locale).
Auto-translation uses MyMemory by default. Incremental translations (new keys only) run automatically during the git pre-commit hook. Use pnpm run i18n:auto-translate --all to force a full re-scan of all missing keys. Use pnpm run check:i18n:branch before large doc-only PRs to lint keys changed vs HEAD without the full unused-key pass.
- VS Code (or Cursor) with TypeScript + ESLint support
- Prettier editor extension (optional convenience; repository already defines formatting rules)
- React DevTools for renderer debugging
-
Install Git (Xcode CLT includes it):
xcode-select --install
-
Install Node 22 (22.13.0+ recommended via nvm) and npm:
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh -o install_nvm.sh less install_nvm.sh bash install_nvm.sh rm install_nvm.sh export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" nvm install 22 nvm use 22
git clone https://github.com/Colorado-Mesh/mesh-client
cd mesh-client
pnpm install
pnpm run devPodman Desktop is the preferred cross-platform Docker-compatible engine for local CI. When Docker compatibility is enabled, Podman exposes a Docker-compatible socket at /var/run/docker.sock; pass that path to act via ACT_DOCKER_SOCKET, or let it auto-detect if Podman Desktop created the socket symlink. If you use Docker Desktop instead, see the socket note above.
On first BLE connection, macOS prompts for Bluetooth access. If denied accidentally:
- Go to System Settings > Privacy & Security > Bluetooth
- Enable access for Mesh-Client
If you work on the Reticulum protocol tab, install Rust and build the sidecar — see Reticulum sidecar (optional) above. On macOS, rustup is preferred over brew install rust (CI parity); do not install both.
If a downloaded app reports "Mesh-client is damaged and can't be opened", see macOS: File is damaged and cannot be opened.
- Install Git and Node.js (winget primary path):
winget install git.git winget install OpenJS.NodeJS
- Allow npm script execution in current user scope:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
- Install Visual Studio Build Tools with Desktop development with C++ workload.
- Install Python 3 and ensure it is on PATH:
If needed, set npm Python path explicitly:
winget install Python.Python.3.12npm config set python "C:\\Path\\To\\python.exe"
git clone https://github.com/Colorado-Mesh/mesh-client
cd mesh-client
pnpm install
pnpm run devThe Windows build (dist:win) uses pnpm's node-linker=hoisted mode to work around asar packaging issues on Windows. The build command automatically reinstalls with hoisted mode, packages, then restores the default structure.
If serial ports do not appear, install the right USB UART driver (for example CH340/CH341, CP210x, or FTDI).
See troubleshooting.md (Visual Studio), Python, and dist:win path / EPERM.
Install Node 22 (22.13.0+ recommended), make, and C++ build tools (g++/gcc-c++) with native build dependencies.
Debian/Ubuntu:
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
nvm install 22
nvm use 22
sudo apt install build-essential
sudo apt install python3 libnspr4 libnss3Fedora/RedHat:
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
nvm install 22
nvm use 22
sudo dnf install @development-tools
sudo dnf install python3 nspr nssgit clone https://github.com/Colorado-Mesh/mesh-client
cd mesh-client
pnpm install
pnpm run devAdd your user to dialout:
sudo usermod -a -G dialout $USERLog out/in after changing groups.
Linux uses Web Bluetooth (Chromium's built-in BLE API) instead of @stoprocent/noble. This approach:
- Requires no setcap/setuid workaround scripts
- Requires the user to select a device from the in-app Bluetooth picker (backed by Chromium's chooser event)
- Requires a user gesture (button click) to trigger device selection
The app automatically enables --enable-experimental-web-platform-features on Linux at startup.
There is no portable Web Bluetooth API for the negotiated ATT MTU (WebBluetoothCG#383). When Chromium exposes maximumWriteValueLength on the TX characteristic, the client chunks writeValue accordingly; otherwise it sends each payload in one call.
Pairing failures and BlueZ steps: BLE known issues.
The supported dev and local run flows are:
pnpm run dev
pnpm startARM (for example Raspberry Pi) may also require:
sudo apt install zlib1g-dev libfuse2
sudo sysctl -w kernel.unprivileged_userns_clone=1See Linux development: SIGILL / SIGSEGV and Linux: serial port access denied.