Development Environment Setup
This guide covers local development setup for Mesh Client, including cloning, prerequisites, test harness tooling, and OS-specific troubleshooting.
Shared Requirements and Tooling
These requirements apply to all platforms.
1) Required software
- Git
- Node.js 22.13.0+ and pnpm 10+ (
package.jsonengines;.npmrcsetsengine-strict=truesopnpm installfails on version mismatch) - CI uses Node 22
- Python 3 +
pip(needed for MkDocs documentation build and yamllint)
Verify:
git --version
node --version
pnpm --version
MkDocs (documentation) tooling
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
- 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.
2) Clone and install
git clone https://github.com/Colorado-Mesh/mesh-client
cd mesh-client
pnpm install
If 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
3) Run the app
- Dev mode (hot reload):
pnpm run dev - Production-like local start:
pnpm start
Common pnpm commands
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 test:run
pnpm run lint
pnpm run typecheck
pnpm run format:check
# Docs
pnpm run docs:install
pnpm run docs:build
pnpm run docs:serve
All Scripts Reference
Complete reference of all pnpm scripts in package.json, organized by category.
Build
| 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/ |
Run
| Script | Description |
|---|---|
dev |
Hot-reload dev mode: builds main/preload in watch mode + Vite dev server + Electron |
start |
Production-like local start: runs build then launches Electron |
electron:open |
Launch Electron (requires prior build) |
trace-deprecation |
Run with Node deprecation traces enabled |
Package (distributables)
| Script | Description |
|---|---|
dist |
Build for current platform |
dist:mac |
Build macOS .dmg + .zip → release/ |
dist:mac:publish |
Build macOS and upload to release server |
dist:linux |
Build Linux .AppImage + .deb → release/ |
dist:linux:publish |
Build Linux and upload to release server |
dist:win |
Build Windows .exe installer → release/ |
dist:win:publish |
Build Windows and upload to release server |
Test
| Script | Description |
|---|---|
test |
Run tests in watch mode |
test:run |
Run tests once (CI mode) |
test:verbose |
Run tests with verbose output |
Lint / Format
| Script | Description |
|---|---|
lint |
Run ESLint (type-aware) |
lint:fix |
Run ESLint with auto-fix |
lint:md |
Run markdownlint-cli2 on all .md files |
format |
Format all code via Prettier |
format:check |
Check formatting without fixing |
Typecheck
| Script | Description |
|---|---|
typecheck |
TypeScript check: renderer + main process |
Quality Checks
| Script | Description |
|---|---|
check:log-injection |
Detect unsanitized user data in log calls |
check:db-migrations |
Verify SQLite migrations are valid |
check:i18n |
Verify all UI strings have English keys and locale coverage |
check:ipc-contract |
Verify IPC channel contracts between main/preload/renderer |
Documentation
| Script | Description |
|---|---|
docs:install |
Install MkDocs Python dependencies |
docs:build |
Build static docs to site/ |
docs:serve |
Serve docs locally with live reload |
Setup / Helpers
| Script | Description |
|---|---|
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) |
i18n:auto-translate |
Machine-translate missing keys via MyMemory |
rebuild |
Rebuild native Node modules for Electron |
Lifecycle (automatic)
| Script | Description |
|---|---|
preinstall |
Enforce pnpm as package manager |
postinstall |
Rebuild native modules + apply patches |
prepare |
Enable git hooks |
predist |
Dedupe packages before packaging |
Dependabot dependency updates
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:run
Do not use npm install; it creates a package-lock.json and may not respect pnpm's lockfile format.
4) Test harness setup and local quality checks
This section is the project test harness setup.
Installed via pnpm install (from package.json):
vitestand renderer/main test dependencieseslinttypescriptprettierprettier-plugin-shmarkdownlint-cli2
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)dockerandact(only if you run GitHub Actions locally)- Python 3 +
venv+ MkDocs Python deps (for docs checks/builds)
Run these quality checks before opening a PR:
Quality checks
pnpm run test:run pnpm run lint pnpm run lint:md pnpm run typecheck pnpm run format:check pnpm run check:i18n
Other useful test commands:
- `pnpm test` (watch mode)
- `pnpm run test:verbose` (verbose failures)
- `pnpm run i18n:auto-translate` (fill missing keys)
### 5) Building a distributable
Use the platform-specific packaging command:
```bash
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.
Build analysis
To analyze the main process bundle size and composition:
pnpm run build:main:minify-meta
This 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
6) Git hooks and pre-commit behavior
After pnpm install, repo hooks are enabled via core.hooksPath and pre-commit runs checks (format, lint, typecheck, audit, actionlint, tests).
Emergency bypass is available:
git commit --no-verify
Use this only as a temporary escape hatch, then run the skipped checks manually as soon as possible.
7) CI workflow tooling (optional but recommended)
- Docker (required to run
actlocally) - act: run GitHub Actions locally with Linux amd64 parity:
act --container-architecture linux/amd64 -P ubuntu-latest=ghcr.io/catthehacker/ubuntu:full-latest
- actionlint: required for local pre-commit if workflow files are touched.
8) Helper scripts (auto-install where possible)
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.
- 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 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).
9) Internationalization (i18n)
The app uses i18next for localization. English is the source of truth.
- Locale files:
src/renderer/locales/{en,es,...}/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.
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.
10) Optional editor/tooling
- VS Code (or Cursor) with TypeScript + ESLint support
- Prettier editor extension (optional convenience; repository already defines formatting rules)
- React DevTools for renderer debugging
macOS
Install prerequisites
- Install Git (Xcode CLT includes it):
bash xcode-select --install - Install Node 22 (22.13.0+ recommended via nvm) and npm:
bash 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
Build/run flow
git clone https://github.com/Colorado-Mesh/mesh-client
cd mesh-client
pnpm install
pnpm run dev
Bluetooth permissions
On first BLE connection, macOS prompts for Bluetooth access. If denied accidentally:
- Go to System Settings > Privacy & Security > Bluetooth
- Enable access for Mesh-Client
macOS release-download note (not required for source development)
If a downloaded app reports "Mesh-client is damaged and can't be opened", remove quarantine:
xattr -r -d com.apple.quarantine /Applications/Mesh-client.app
Windows
Install prerequisites
- Install Git and Node.js (winget primary path):
powershell winget install git.git winget install OpenJS.NodeJS - Allow npm script execution in current user scope:
powershell 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:
powershell winget install Python.Python.3.12If needed, set npm Python path explicitly:powershell npm config set python "C:\\Path\\To\\python.exe"
Build/run flow
git clone https://github.com/Colorado-Mesh/mesh-client
cd mesh-client
pnpm install
pnpm run dev
Windows packaging note
The 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.
Serial device driver reminder
If serial ports do not appear, install the right USB UART driver (for example CH340/CH341, CP210x, or FTDI).
Troubleshooting
"Could not find any Visual Studio installation to use"
Cause: outdated node-gyp resolution or missing C++ build tools workload.
Fix:
- Install/confirm Visual Studio Build Tools with Desktop C++ workload.
- Upgrade node-gyp:
bash pnpm install node-gyp@latest -g pnpm install node-gyp@latest --save-dev - Restart terminal and rerun:
bash pnpm install
"Could not find any Python installation to use"
Cause: Python missing or not on PATH for node-gyp.
Fix:
- Install Python 3 and add it to PATH.
- Restart terminal.
- Retry
pnpm install(orpnpm run dist:win). - If still failing, set Python path with
npm config set python ....
dist:win fails with path spaces or EPERM
- Prefer a path without spaces (for example
C:\dev\mesh-client) - Close running Electron/Node processes before rebuild
- Run:
bash pnpm run rebuild pnpm run dist:win
Linux
Install prerequisites
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 libnss3
Fedora/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 nss
Build/run flow
git clone https://github.com/Colorado-Mesh/mesh-client
cd mesh-client
pnpm install
pnpm run dev
Serial permissions
Add your user to dialout:
sudo usermod -a -G dialout $USER
Log out/in after changing groups.
Linux Bluetooth (BLE)
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.
Bluetooth Pairing on Linux
Web Bluetooth may invoke the Electron pairing handler during GATT connect. Behavior differs by protocol:
- Meshtastic: On the first Chromium
providePinrequest, the client tries the standard Meshtastic PIN123456. If that fails, you are prompted to enter the PIN manually. - MeshCore: The MeshCore session does not auto-submit
123456. When Chromium asks for a PIN, you enter the random code shown on the radio.
MeshCore and OS-level pairing (Linux / BlueZ): A stable GATT session usually requires a bond in BlueZ first. After you choose a device in the in-app picker, the client runs bluetoothctl info <MAC>. If the device is not paired (Paired: no) or not yet known to the adapter, the UI prompts for the PIN on the radio and runs bluetooth-pair (main-process bluetoothctl pairing) before resolving the pending Web Bluetooth selection. If Paired: yes already, connection continues without that step.
Handshake retries reuse the same granted Web Bluetooth device (navigator.bluetooth.getDevices()) so the second attempt does not call requestDevice() without a user gesture.
If you encounter pairing issues (e.g., "Connection attempt failed" or device was previously paired with wrong PIN):
- Use the "Remove & Re-pair Device" button in the app
- Or manually remove via
bluetoothctl:bash bluetoothctl # Inside bluetoothctl: remove XX:XX:XX:XX:XX:XX # Replace with your device MAC # Then re-pair from the app - If the device still won't connect, power cycle Bluetooth:
bash bluetoothctl power off bluetoothctl power on
Linux launch notes
The supported dev and local run flows are:
pnpm run dev
pnpm start
ARM (for example Raspberry Pi) may also require:
sudo apt install zlib1g-dev libfuse2
sudo sysctl -w kernel.unprivileged_userns_clone=1
Troubleshooting
SIGILL during pnpm install (electron exited with signal SIGILL)
Install without running Electron rebuild first:
MESHTASTIC_SKIP_ELECTRON_REBUILD=1 pnpm install
Then run rebuild on a host where Electron executes correctly:
pnpm run rebuild
SIGSEGV on startup (electron exited with signal SIGSEGV)
Use:
pnpm run build && pnpm dlx electron . --disable-gpu
Or:
pnpm run electron:open -- --disable-gpu
Optional persistent mitigation:
export MESH_CLIENT_DISABLE_GPU=1ELECTRON_OZONE_PLATFORM_HINT=x11 pnpm run electron:open
Serial: serial_io_handler.cc:147 Failed to open serial port: FILE_ERROR_ACCESS_DENIED
- Ensure user is in
dialout. - Re-login.
- Verify with:
bash groups - If missing, create and activate:
bash sudo groupadd dialout sudo usermod -a -G dialout $USER newgrp dialout