Development¶
Guide for setting up a development environment and contributing to decky-romm-sync.
Prerequisites¶
- mise — manages Node, pnpm, and Python versions
- Git
- A Steam Deck or Linux PC with Decky Loader installed (for testing)
Setup¶
git clone https://github.com/danielcopper/decky-romm-sync.git
cd decky-romm-sync
mise install # installs Node LTS, pnpm, Python
mise run setup # installs JS + Python dependencies
This creates a Python virtual environment (auto-activated by mise via _.python.venv in mise.toml) and installs all npm packages.
Building¶
The frontend is bundled with Rollup into a single dist/index.js file that Decky Loader serves.
Testing¶
To run with coverage:
Tests mirror the source layout (tests/services/, tests/adapters/, tests/domain/, tests/models/, tests/lib/), with each test file mapping 1:1 to a source module. Shared mocks live in tests/conftest.py, which also provides a mock decky module so tests run without Decky Loader.
Frontend component tests run with mise run test:frontend (pnpm test); see the CLAUDE.md "Frontend component tests" section for the @decky/api event harness.
Every backend feature or callable where testing makes sense should have unit tests covering:
- Happy path — normal successful operation
- Bad path — invalid input, missing data, API errors, network failures
- Edge cases — empty strings, None values, boundary conditions
Dev Reload¶
This runs pnpm build and then sudo systemctl restart plugin_loader to pick up changes. For backend-only changes, restarting the plugin loader is sufficient without rebuilding.
Deploying to Device¶
For development, symlink the repo into the plugins directory:
This way, rebuilds take effect immediately after a Decky restart.
Linting¶
PYTHONPATH=py_modules lint-imports # check service/adapter layer rules
mise run lint # same via mise
The .importlinter config enforces the layer boundary contracts:
- Services must not import concrete adapter implementations (Protocols are allowed)
- Adapters must not import services
- Utilities (
lib/) must not import services, adapters, or domain - Domain must not import services or adapters (
libis allowed) - Models must not import services, adapters, domain, or lib
- Services must not import stdlib I/O / non-deterministic primitives (
time,uuid,random,subprocess,threading,requests) - Services must be independent of each other (no cross-service imports)
mise run lint also runs scripts/check_cosmic_call_bans.sh, which complements the import rules at the call site: services may not call datetime.now() / asyncio.sleep() / time.time() / time.monotonic() / uuid.uuid4() / random.* directly — they inject the Clock / Sleeper / UuidGen Protocol instead.
See Backend Architecture for details.
Code Quality¶
- SonarCloud — CI-based analysis on every PR and push to main. Quality Gate enforces 80% coverage on new code, 0 bugs, 0 vulnerabilities.
- Ruff — Python linting in CI. Expanded ruleset includes B (bugbear), SIM (simplify), UP (pyupgrade), RUF (ruff-specific), and ARG (unused arguments) in addition to the base E/F rules.
- basedpyright — Type checking in CI. Checks all source files including the test suite (tests/ is not excluded).
- import-linter — Layer boundary enforcement in CI (see Linting section above).
- pytest-cov — Branch coverage reported to SonarCloud.
Project Structure¶
main.py # Plugin entry — Decky lifecycle + callable surface
py_modules/
bootstrap.py # Composition root — bootstrap() builds adapters, wire_services() builds services
services/ # Orchestration / business logic (Protocol-typed deps via *ServiceConfig)
protocols/ # Protocol interfaces, grouped: transport / determinism /
# persistence / paths / infra / files / cross_service
library/ # LibraryService façade — fetcher, sync_orchestrator, reporter, shared state box
saves/ # SaveService aggregate — state, sync_engine/, slots/, status/, versions
downloads.py # DownloadService — ROM downloads, ZIP/M3U, fcntl queue
firmware.py # FirmwareService — BIOS registry + downloads
session_lifecycle.py # SessionLifecycleService — post-exit orchestration
migration.py # MigrationService — RetroDECK path + save-sort migration
steamgrid.py # SteamGridService — SteamGridDB artwork
artwork.py # ArtworkService — cover art staging/cleanup
game_detail.py / playtime.py / achievements.py / settings.py / cores.py
metadata.py / rom_removal.py / shortcut_removal.py / launch_gate.py
startup_healing.py / connection.py
adapters/ # I/O boundaries — implement Protocols
romm/{http,romm_api}.py # RomM HTTP transport + REST adapter
steam_config.py / steamgriddb.py / sgdb_artwork_cache.py / cover_art_file_store.py
persistence.py / registry_store.py / metadata_cache_store.py
download_file.py / firmware_file.py / migration_file.py / rom_files.py / save_file.py
retrodeck_paths.py / retroarch_config.py / retroarch_core_info.py / es_de_config.py
system_clock.py / system_uuid_gen.py / asyncio_sleeper.py / hostname.py / path_probe.py / plugin_metadata.py / debug_logger.py
domain/ # Pure compute — no I/O, no service/adapter imports
sync_action.py / sync_diff.py / preview_delta.py / work_unit.py
save_state.py / save_path.py / save_status*.py / save_attribution.py / save_extensions.py
firmware_paths.py / bios.py / achievements.py / shortcut_data.py / steam_categories.py
sgdb_artwork.py / installed_roms.py / rom_files.py / retroarch_core_info.py
state_migrations.py / sync_state.py / emulator_tag.py / version.py
models/ # Data shapes (TypedDicts/dataclasses) — independent of other layers
lib/ # Cross-cutting utilities (errors, list_result, iso_time, path_safety, late_binding, ...)
_vendor/ # Vendored third-party deps — not our code, only imported by adapters
vdf/ # Valve Data Format parser (Steam shortcuts.vdf)
src/ # Frontend TypeScript
index.tsx # Plugin entry, event listeners, QAM router
components/ # React components (QAM pages, game detail UI)
patches/ # Route and store patches
api/backend.ts # callable() wrappers (typed)
types/ # TypeScript interfaces and Steam API declarations
utils/ # Shortcut CRUD, sync, downloads, collections, session manager
bin/romm-launcher # Bash launcher for RetroDECK
defaults/config.json # platform_map: 149 platform slug -> RetroDECK system mappings
tests/ # Backend unit tests, mirroring py_modules/ layout
See Backend Architecture for the service/adapter design, dependency diagram, and layer enforcement rules.