# @punktfunk/plugin-rom-manager A [Punktfunk](https://git.unom.io/unom/punktfunk) plugin that scans your ROM folders, maps them to emulators, fetches box art, and reconciles everything into the host game library — so your retro games appear automatically in the Punktfunk web console, native clients, and Moonlight `/applist`, each launchable and covered in art. It ships a web UI that lives **inside the Punktfunk console** (no second password, no second port). Think of it as [Steam ROM Manager](https://github.com/SteamGridDB/steam-rom-manager) for Punktfunk: it even uses the same art source (SteamGridDB) when you give it an API key. --- ## Requirements - A Punktfunk host (Linux or Windows) with the **scripting runner** (`punktfunk-scripting`) enabled. - [Bun](https://bun.sh) (the runner is Bun). - `@punktfunk/host` **≥ 0.1.2** + `@punktfunk/plugin-kit` **≥ 0.1.1** — it provides the plugin-UI surface (`servePluginUi`) and discovers scoped `@punktfunk/plugin-*` packages. ## Install The plugin is scoped under `@punktfunk`, so one bunfig line points that scope at the Punktfunk registry while everything else (like `effect`) resolves from npm — no bundling, and `@punktfunk/host` + `effect` are shared across all your plugins: ```sh mkdir -p "$(punktfunk config-dir)/plugins" && cd "$(punktfunk config-dir)/plugins" cat > bunfig.toml <<'EOF' [install.scopes] "@punktfunk" = "https://git.unom.io/api/packages/unom/npm/" EOF bun add @punktfunk/plugin-rom-manager # then enable the runner (opt-in): systemctl --user enable --now punktfunk-scripting # Linux Enable-ScheduledTask PunktfunkScripting # Windows ``` Open the Punktfunk console — a **ROM Manager** entry appears in the nav. That's it. > **Headless / host-only boxes** without the console can just drop a `config.json` (below) and use > the CLI — the sync engine is fully functional from the file alone. ## Quick start (config file) The plugin owns `/plugin-state/rom-manager/config.json`. A minimal example: ```jsonc { "roots": [ { "dir": "/mnt/roms/snes", "platform": "snes" }, { "dir": "/mnt/roms/ps1", "platform": "ps1", "excludes": ["*.sav", "bios/**"] } ], "art": { "provider": "auto", "steamGridDbKey": "" } } ``` Only the keys you author are stored — defaults live in the schema (`contract/src/config.ts`) and are never baked into your file. Then, from the plugin dir: ```sh bunx punktfunk-plugin-rom-manager scan # what the scanner finds bunx punktfunk-plugin-rom-manager detect # which emulators are installed bunx punktfunk-plugin-rom-manager preview # the desired library (no host write) bunx punktfunk-plugin-rom-manager sync # reconcile into the live library ``` ## Box art Two sources, selected by `art.provider`: | Provider | Key? | Art types | Notes | |---------------|-------|------------------------------------|-------| | `steamgriddb` | yes | portrait + hero + logo + header | What Steam ROM Manager uses; best coverage + fuzzy title match. Free key from [steamgriddb.com › profile › preferences](https://www.steamgriddb.com/profile/preferences). | | `libretro` | no | box-art portrait only | Keyless [libretro-thumbnails](https://thumbnails.libretro.com); works out of the box. | | `auto` (default) | — | SteamGridDB if a key is set, else libretro | | Art URLs are handed to the host as-is; native clients/console fetch them directly (so **clients need internet for covers**), and Moonlight covers are proxied host-side. Verdicts are cached in `cache.json` and never re-probed unless you change provider/key. ## Platforms & emulators ~25 built-in platforms (NES, SNES, N64, GameCube, Wii, Switch, GB/GBC/GBA, NDS, 3DS, PS1/2/PSP, Genesis, Master System, Game Gear, Saturn, Dreamcast, PC Engine, Neo Geo Pocket, WonderSwan, Lynx, Atari 2600/7800, Xbox). Arcade (MAME/FBNeo romset versioning) is intentionally out of scope. Emulators are detected best-effort (PATH → Flatpak → known install paths): RetroArch (+ cores), Dolphin, PCSX2, DuckStation, PPSSPP, mGBA, melonDS, Flycast, xemu, plus Azahar/Ryujinx. Any platform's emulator, core, and args are overridable per-platform and per-game (config or UI). Missing an emulator? Add a `custom-*` template. Disc games are folded so a set is **one entry**: `.m3u` playlists absorb their discs, `.cue`/`.gdi` sheets hide their tracks, `.chd` stands alone. ## The UI `servePluginUi` serves the SPA on a loopback ephemeral port behind a per-boot secret and registers it with the host; the console reverse-proxies it and gates it with your existing console sign-in. Five pages: **Overview** (health, stats, live sync activity), **Library** (scan preview with covers + include toggles), **Sources** (ROM roots), **Emulators** (detection + per-platform overrides), **Settings** (art, sync, file paths). The console surface is the only UI — the old standalone password server was removed in 0.3.0 (headless boxes drive the plugin via the CLI). ## Security - **ROM filenames are untrusted input** that ends up in a `sh -c` / `cmd.exe /c` string run as the host user. Every path goes through strict per-OS quoting (`src/quote.ts`); on Windows, names containing `cmd.exe` metacharacters (`" % ! ^ & | < >`) are refused rather than risk injection. - `config.json` lives in the hardened ``; the plugin refuses a group/world-writable one. - Outbound network in v1: SteamGridDB (if keyed) and libretro-thumbnails only. No telemetry. ## Development — this repo is the plugin blueprint rom-manager is the reference consumer of [`@punktfunk/plugin-kit`](https://git.unom.io/unom/punktfunk/src/branch/main/plugin-kit) — copy this repo's structure to start a new plugin. Three bun workspaces: | Workspace | What it is | |---|---| | `contract/` | **The single source of truth**: Effect Schemas for the config (raw↔resolved in one schema), domain DTOs, the `HttpApi` contract, API errors. Never published — bundled into the plugin, imported source-level by the UI. No hand-mirrored types anywhere. | | `plugin/` | The published package. `src/domain` + `src/art` are the pure, injectable, unit-tested core; `src/services` wires them into the kit (ConfigService, CacheStore, SyncEngine, ProviderClient, HttpApi handlers + SSE); `src/index.ts` is the `definePluginKit` entry (async at the runner boundary, Effect inside). | | `ui/` | The SPA: React 19 + `@unom/ui` + `@unom/app-ui` + the kit's `/react` helpers + `/theme.css`, with an Effect-native data layer (`AtomHttpApi` atoms derived from the contract). Builds into `plugin/dist/ui`. | ```sh bun install # one workspace install (Gitea registry for @punktfunk/@unom scopes) bun test # domain + service tests (plugin/) bun run typecheck # contract → plugin → ui bun run build # backend bundle + SPA → plugin/dist cd ui && bun run dev # UI with ZERO host running (in-browser mock layer + scenarios) cd ui && bun run storybook # per-page stories on the same fixtures (port 6013) cd plugin && bun run dev # auth-free dev API on :5885 … cd ui && bun run dev:live # … and the UI proxied against it ``` Requires `@punktfunk/host` ≥ 0.1.2 and `@punktfunk/plugin-kit` ≥ 0.1.1 at runtime (shared with the runner, not bundled). `@unom/*` + `@effect/atom-react` are build-time only — the SPA ships as static assets, so the published plugin carries no UI runtime dependencies. ## License MIT OR Apache-2.0.