This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This repo is Stereum 2.0, an Ethereum node setup & manager. It has two independent halves:
launcher/— an Electron + Vue 3 desktop app (the "GUI"). This is where almost all JavaScript development happens. Allnpmcommands below are run from insidelauncher/.controls/— Ansible roles and playbooks (the "backend automation") that actually install and manage node services on a remote server, plus a Molecule/Poetry test harness for them.
The launcher never runs node software locally. It SSHes into a target server, copies the controls/ Ansible roles there, and runs playbooks remotely. Understanding this split is essential.
npm i # install deps (runs electron-builder install-app-deps postinstall)
npm run dev # start the app in dev mode (electron-vite: renderer HMR + main auto-restart)
# stereum / electron:serve / backend:watch are all aliases of `electron-vite dev`
npm run build # electron-vite build -> out/{main,preload,renderer} (no packaging)
npm run electron:build # electron-vite build + electron-builder -> dist/<platform> (publish never)
npm run lint:fix # eslint src --fix (lint only files you changed)
npm run format # prettier . --write (required before PRs)
npm run format:check # prettier . --check (CI gate)
# Tests (Jest). testMatch = *.test.js | *.spec.js | *.int.js
npm run test:unit # unit tests (.test/.spec) with coverage
npm run test:int # integration tests (.int.js) — see warning below
npx jest path/to/File.test.js # run a single test file
npx jest -t "geth installation" # run tests matching a nameIntegration tests (*.int.js) are expensive and side-effectful: each spins up a real Hetzner Cloud server via HetznerServer.js, installs Stereum over SSH, and asserts on the running node. They require HCLOUD_TOKEN in the environment and take minutes each (jest.setTimeout(600000)). Do not run them casually. npm run test:cleanup (runs src/backend/tests/integration/Cleanup.js) tears down leftover servers. Regular unit tests run fully offline against jsdom.
Uses Poetry + Molecule (see controls/README.md). Roles update-changes, configure-updates, configure-firewall use the docker driver (local); all others use the hcloud driver and need HCLOUD_TOKEN.
cd controls && poetry install
cd controls/roles/<role> && poetry run molecule test [-s <scenario>]- Main process (
src/background.js): owns all backend logic and registers everyipcMain.handle(...)channel (connect,setup,getServices,updateServices, …). This is the single catalog of what the renderer can ask the backend to do. - Preload (
src/preload.js): exposes a minimalwindow.promiseIpc(wrappingipcRenderer.invoke) plus terminal/custom-URL bridges viacontextBridge. Context isolation is on — the renderer has no direct Node access. - Renderer → backend: the renderer calls methods on
src/store/ControlService.js, a singletonEventEmitterthat forwards each call aspromiseIpc.send("<channel>", args). So a new backend capability = newipcMain.handleinbackground.jsand a matching method inControlService.js.
NodeConnection.js(large, central): owns the SSH connection lifecycle, detects the server OS, reads/writes Stereum settings, installs the Ansible roles onto the server, and runs playbooks.runPlaybook(role, extraVars)executescontrols/genericPlaybook.yamlremotely withstereum_role=<role>and JSON extra-vars, using the customstereumjsonAnsible stdout callback for machine-readable results keyed by a randomplaybookRunRef.SSHService.js: connection pool +exechelpers;checkExecError/extractExecErrorare the standard way error results are surfaced.ServiceManager.js(very large): the heart of service configuration — creates/modifies/wires services together (dependencies, ports, volumes, client pairings).ethereum-services/: one class per supported client/tool (GethService,LighthouseBeaconService,PrysmValidatorService,FlashbotsMevBoostService, Optimism/L2 variants, SSV, Lido, monitoring exporters, …), all extendingNodeService.js. Each service builds a Docker container spec: it hasbuildByUserInput(network, ports, dir)/buildByConfiguration(...)factories and generates the container command/entrypoint/volumes. Supported networks live inNodeService.networks. When adding or changing a client, this is where the container command/flags are defined — and there's typically a matching*.test.js(unit) and*.int.js(integration) file.- Other notable pieces:
OneClickInstall.js(predefined "constellation" setups),ValidatorAccountManager.js,Monitoring.js(large; Grafana/Prometheus data),ConfigManager.js,TaskManager.js(tracks running playbooks),NodeUpdates.js.
- Vue 3 +
vue-router+ Pinia stores (src/store/, e.g.theNode,services,nodeManage,servers).ControlService.jsalso lives here as the IPC gateway. src/pages/= routed screens;src/components/(UI/base/layers);src/composables/= reusable logic (services.js,monitoring.js,useTerminal.js, etc.).- i18n via
vue-i18n; translations insrc/languages/(managed by Crowdin, seecrowdin.yml). - Styling: Tailwind (
tailwind.config.js) processed throughpostcss.config.js;src/main.css(with@tailwinddirectives) is imported bysrc/main.js.
The app is bundled with electron-vite (electron.vite.config.js), which drives three Vite builds — main (entry src/background.js), preload (src/preload.js), and renderer (root index.html + src/main.js) — into out/{main,preload,renderer}. electron-builder.config.cjs then packages out/ into dist/<platform>.
- Renderer loads over
http://localhostin dev and a customapp://scheme in production (registered inbackground.js), so absolute asset paths like/img/...resolve against the bundle.ELECTRON_RENDERER_URLdistinguishes dev from prod. - Main/preload keep runtime deps external (
externalizeDepsPlugin), resolved fromnode_modulesat runtime;electron-storeis bundled (ESM-only). The renderer aliases@→srcand needs.vueinresolve.extensionsfor extensionless imports. - Renderer code must use ESM
import(norequire); static assets underpublic/are referenced by root-relative string paths (/img/...), notrequire(...).
At runtime on the target server, genericPlaybook.yaml merges three layers into a single stereum var: controls/defaults/stereum_defaults.yaml (static defaults incl. pinned client versions:), /etc/stereum/stereum.yaml (per-server settings), and command-line stereum_args. Client image versions are pinned in stereum_defaults.yaml and as imageVersion defaults inside each *Service.js — keep them coherent when bumping.
- Prettier + ESLint config are the source of truth (
.prettierrc.json,eslint.config.js). Runnpm run formatbefore any PR (format:checkis a CI gate). Lint only files you touched. - Style:
camelCasefunctions/vars,PascalCaseclasses,UPPERCASE_WITH_UNDERSCORESconstants, double quotes. - Conventional-commit-style PR titles (imperative, no trailing period).
- CI (
.github/workflows/) runs Jest unit tests, Molecule role tests, integration tests, ESLint/Prettier, andshellcheckon shell scripts. Shell scripts incontrols/must pass shellcheck.