Setup
Prerequisites
Section titled “Prerequisites”- Node.js 24 or newer
- pnpm (install with
npm install -g pnpm) - Windows (required for the native addon and iRacing integration)
- Stream Deck software 7.1 or newer — required to run the Elgato plugin
- HotSpot StreamDock (or another QT5 SVG Tiny 1.2 compatible Mirabox host) — only required if you’re developing or testing the Mirabox plugin
- UlanziStudio — only required if you’re developing or testing the Ulanzi Deck plugin
Getting Started
Section titled “Getting Started”# Clone the repositorygit clone https://github.com/niklam/iracedeck.gitcd iracedeck
# Install dependenciespnpm install
# Build all packagespnpm buildAfter the initial install and build, link the plugin to whichever host(s) you’re developing against.
Stream Deck
Section titled “Stream Deck”# Link the built plugin to the Stream Deck softwarepnpm link:stream-deck
# Watch mode — auto-rebuilds on savepnpm watch:stream-deckRestart the Stream Deck software. The plugin appears in the action list as iRaceDeck.
Mirabox
Section titled “Mirabox”# Link the built plugin to the Mirabox hostpnpm link:mirabox
# Watch mode for the Mirabox plugin (no root-level shortcut yet)pnpm --filter @iracedeck/iracing-plugin-mirabox run watchOn Windows, link:mirabox defaults to the standard HotSpot StreamDock install path (%APPDATA%\HotSpot\StreamDock\plugins). If you’re using a different host (e.g. VSD Craft), point MIRABOX_PLUGINS_DIR at its plugins directory in .env.local (see below).
Ulanzi Deck
Section titled “Ulanzi Deck”link:ulanzi junction-links the built packages/iracing-plugin-ulanzi/com.ulanzi.iracedeck.ulanziPlugin folder into UlanziStudio’s plugins directory, defaulting on Windows to %APPDATA%\Ulanzi\UlanziDeck\Plugins. Override with ULANZI_PLUGINS_DIR in .env.local (see below).
# The whole test cycle — the order matters, see belowpnpm stop:ulanzi && pnpm switch-test-env:ulanzi && pnpm start:ulanzi
# Watch mode for the Ulanzi plugin (no root-level shortcut yet)pnpm --filter @iracedeck/iracing-plugin-ulanzi run watchStop the host first: the relink runs inside switch-test-env:ulanzi, straight after the build, and a running UlanziStudio holds the plugin folder’s files open. UlanziStudio also reads its plugins directory only at start, so a relink or a rebuild always needs a restart to take effect. A running host does not make the build itself fail: when it has a native addon loaded, the build moves that file aside and builds a fresh one.
The junction points at exactly one worktree, so the host belongs to whichever worktree ran the last successful link or relink — a link that failed because the destination was already occupied leaves the previous junction untouched. start:ulanzi prints the current target to save you debugging someone else’s build.
.env.local
Section titled “.env.local”Some dev scripts read per-developer settings from .env.local at the repo root. The file is gitignored and never committed. Copy .env.local.example as a starting point:
cp .env.local.example .env.localKeys currently supported:
| Key | Used by | Purpose |
|---|---|---|
MIRABOX_PLUGINS_DIR |
pnpm link:mirabox / unlink:mirabox / relink:mirabox |
Path to your Mirabox host’s plugins directory. Optional on Windows when using HotSpot StreamDock — the scripts default to %APPDATA%\HotSpot\StreamDock\plugins. Set this only to override (e.g. C:\Users\you\AppData\Roaming\VSD Craft\Plugins). |
ULANZI_PLUGINS_DIR |
pnpm link:ulanzi / unlink:ulanzi / relink:ulanzi |
Path to UlanziStudio’s plugins directory. Optional on Windows — the scripts default to %APPDATA%\Ulanzi\UlanziDeck\Plugins. |
MIRABOX_APP_PATH |
pnpm start:mirabox / stop:mirabox |
Path to your Mirabox host executable. Optional on Windows — defaults to %ProgramFiles(x86)%\StreamDock\StreamDock.exe. Set it if you run VSD Craft instead (C:\Program Files (x86)\VSD Craft\VSD Craft.exe). |
ULANZI_APP_PATH |
pnpm start:ulanzi / stop:ulanzi |
Path to the UlanziStudio executable. Optional on Windows — defaults to %ProgramFiles(x86)%\Ulanzi Studio\UlanziDeck.exe. |
Picking up changes after a rebuild
Section titled “Picking up changes after a rebuild”Both Stream Deck and Mirabox host apps cache plugin metadata aggressively. If your changes affect manifest.json, Property Inspector templates, static icons, or the action registration layer, the reliable flow is:
- Quit the host software from the task bar (right-click the tray icon → Quit / Exit). Closing the window is not enough — the app keeps running in the background.
- Run
pnpm build(or let your watch process rebuild automatically). - Start the host software again.
For pure action-code edits, the watch process plus the host’s built-in refresh usually suffices, but when in doubt, run the full stop–build–start cycle.
Auditioning a voice change
Section titled “Auditioning a voice change”The plugin ships no voice clips. It plays the Race Engineer voice pack it downloaded into %LOCALAPPDATA%\iRaceDeck\Race Engineer\Voices\default, and it keeps that folder matching the published catalog — reinstalling it when it is missing, replacing a folder it does not recognise, refreshing one whose clips have gone. That is right for a user and unhelpful when you are the one editing the voice, so a development build can be pointed at the packer’s staged output instead.
Turn it on once per machine, by setting the IRACEDECK_DEV_VOICES user environment variable to 1:
setx IRACEDECK_DEV_VOICES 1setx writes the variable into your user environment, but only processes started afterwards see it — so open a new terminal (and restart your editor, if you build from its terminal) before the next build. You can set it through System Properties → Environment Variables instead. The value must be exactly 1 or 0; anything else fails the build, naming the variable, rather than leaving the mode silently off.
From then on, every build of every worktree stages the voice and points the plugins at it. Build once with the deck host stopped, then start it:
pnpm buildEach plugin build depends on a stage:dev-voices task that runs the voice packs through the same radio filter a downloaded pack gets and stages them under packages/audio-assets/dist/voice-packs/ in that worktree; each plugin carries that directory into its bin/config.json as devVoicePacksRoot, and prints [dev-voices] development voice root on via IRACEDECK_DEV_VOICES: … so the mode is visible in the build output. Staging takes about five seconds: the audio assets build already runs every clip through ffmpeg into a per-worktree cache on every build, whether the mode is on or not, so the stage only copies from that cache. The build never writes an archive or touches the committed catalog/default.json, the release contract the download path is verified against, and CI never sets the variable, so the mechanism cannot ship.
Then, with the deck host running, the loop is: edit clips, or the wording in packages/audio-assets/configs/<voice-id>.voice.json, restage, and press Rescan voices in iRaceDeck Settings, which picks up the new stage without a restart:
pnpm stage:voicesRestage rather than rebuild while the host is running. A voice edit makes all three plugin builds run again, and a running host would not load those builds until it restarts anyway — the restage is all a voice edit needs, and Rescan voices picks it up live. pnpm build is for a code change or a mode switch, and the host loads it only when it starts, so stop the host, build, then start it again. There is no watcher on the voice directory, and watch mode does not restage, so run pnpm stage:voices after a voice edit. A change destined for a release still needs the flagless pnpm --filter @iracedeck/audio-assets pack:voice default, and its regenerated catalog entry committed.
What plays is the bytes the packer stages, radio-filtered exactly as a downloaded pack is, never the raw source tree. The plugin scans that directory ahead of the downloaded packs and never installs over what it finds there, so default stops being replaced under you. Two things say the mode is on in the plugin: the plugin log’s Voice packs: development root active, once per start, and the Installed Voices list, where the pack is badged Development build and shows its directory in place of a Remove button — iRaceDeck never deletes from a directory it did not create.
A single worktree can override the machine setting in either direction with pnpm dev:voices, which writes a gitignored dev.local.json at the repo root (dev.local.json.example shows the shape):
pnpm dev:voices off # this worktree plays the downloaded pack, whatever the variable sayspnpm dev:voices on # this worktree is in development mode, even without the variablepnpm dev:voices auto # remove the override: this worktree follows IRACEDECK_DEV_VOICES againEach verb rebuilds the three plugins and relinks the hosts that are linked to this worktree — a host linked to another worktree is reported and left alone, since relinking it would switch that test environment underneath you. Mirabox and UlanziStudio read their plugins directory only at start, so restart whichever of them was relinked; the script prints the commands. If the build fails, the marker is put back as it was. Stop the hosts linked to this worktree before running it: the relink expects them stopped, and they load the new build only at their next start.
Turn the mode off with pnpm dev:voices off before testing the real download path — switch-test-env and the relink:* scripts deliberately leave the marker alone, so nothing else will. Packing a plugin for release (pack:plugin) refuses a build made in development mode.
Useful Commands
Section titled “Useful Commands”| Command | Description |
|---|---|
pnpm build |
Build all packages |
pnpm build:stream-deck |
Build only the Stream Deck plugin |
pnpm watch:stream-deck |
Watch mode — rebuild on file changes (Stream Deck) |
pnpm --filter @iracedeck/iracing-plugin-mirabox run watch |
Watch mode for the Mirabox plugin |
pnpm --filter @iracedeck/iracing-plugin-ulanzi run watch |
Watch mode for the Ulanzi Deck plugin |
pnpm link:stream-deck |
Register the plugin with Stream Deck |
pnpm unlink:stream-deck |
Unregister the Stream Deck plugin |
pnpm relink:stream-deck |
Unlink + link Stream Deck (useful when switching branches) |
pnpm link:mirabox |
Register the plugin with the Mirabox host (uses MIRABOX_PLUGINS_DIR if set, otherwise the Windows HotSpot StreamDock default) |
pnpm unlink:mirabox |
Unregister the Mirabox plugin |
pnpm relink:mirabox |
Unlink + link Mirabox (useful when switching branches) |
pnpm start:mirabox / pnpm stop:mirabox |
Start/stop the Mirabox host app (start prints the linked worktree) |
pnpm link:ulanzi |
Register the plugin with UlanziStudio (uses ULANZI_PLUGINS_DIR if set, otherwise the Windows default) |
pnpm unlink:ulanzi |
Unregister the Ulanzi plugin |
pnpm relink:ulanzi |
Unlink + link Ulanzi (useful when switching branches) |
pnpm start:ulanzi / pnpm stop:ulanzi |
Start/stop UlanziStudio (start prints the linked worktree) |
pnpm switch-test-env |
Install + build + relink for all three platforms |
pnpm switch-test-env:stream-deck |
Install + build + relink only Stream Deck |
pnpm switch-test-env:mirabox |
Install + build + relink only Mirabox |
pnpm switch-test-env:ulanzi |
Install + build + relink only Ulanzi |
pnpm dev:voices on / off / auto |
Override the machine-wide IRACEDECK_DEV_VOICES setting for this worktree: development voices on, off, or back to following the variable — see Auditioning a voice change |
pnpm test |
Run all tests |
pnpm test <path> |
Run the tests whose path contains <path>, e.g. pnpm test packages/iracing-sdk/ |
pnpm --filter <package> test |
Run one package’s tests (also pnpm test inside the package directory) |

