WP6.1 of design/library-scanner-plugins-implementation-plan.md. The library is a flagship surface and cannot depend on an opt-in subsystem (design D9, closing G9): once the scanners are plugins, a host whose runner is off comes up with an empty library and no obvious reason why. The security posture for on-by-default was already built and shipped — LocalService on Windows, a sandboxed systemd --user unit on Linux, the scoped plugin-token lane. Windows (.iss): the PunktfunkScripting task is registered ENABLED and started on a FRESH install, and left to the existing restore path on an upgrade. The distinction is a new TaskExists probe taken before StopBunRuntimes disables anything — TaskEnabled alone cannot tell a fresh install from an operator who deliberately turned the runner off, and defaulting to "on" would silently switch it back on for them. deb/rpm: `systemctl --global enable` from the postinst/%post, guarded to first install only so an upgrade never undoes a mask. `--global` because a maintainer script has no user session to act on, and it is the only mechanism that makes a --user unit on-by-default for everyone. sysext: RPM scriptlets never run from a sysext image, so the enablement symlink is baked in directly (/usr/lib/systemd/user/default.target.wants/). Without it the runner would ship present-but-off on exactly the platform where an operator is least likely to go looking for it. Opt-out throughout is `systemctl --user mask punktfunk-scripting` — `mask`, not `disable`, since a plain disable cannot remove a symlink under /etc or /usr. The unit comment, both package descriptions, and the docs-site plugins page all say so; the page also gains the Windows equivalent. Not gated on hardware: none of this is verifiable from a Mac. The .iss change needs an installer run (fresh + upgrade, and an upgrade with the task deliberately disabled), and the deb/rpm/sysext changes need a package build.
367 lines
18 KiB
Plaintext
367 lines
18 KiB
Plaintext
---
|
|
title: Plugins
|
|
description: First-party plugins — sync your ROM collection or Playnite library into the game library, or hand a real USB device on the couch to the host — and how to install them.
|
|
---
|
|
|
|
Plugins extend the host through the **scripting runner** (see [Events & hooks](/docs/automation)). A
|
|
plugin runs alongside the host, reconciles titles into your [game library](/docs/game-library) as a
|
|
provider — so they appear in the grid on every client — and can add its own page to the [web console](/docs/web-console).
|
|
|
|
Three first-party plugins today:
|
|
|
|
| Plugin | What it does |
|
|
|---|---|
|
|
| **ROM Manager** | Scans your ROM directories, matches each platform to an installed emulator, and syncs them into the library with box art. |
|
|
| **Playnite** | Mirrors your [Playnite](https://playnite.link) library — every store and emulator it manages — into the library, launched back through Playnite. |
|
|
| **VirtualHere** | Hands a real USB device on the couch — wheel, HOTAS, pad — to the host while you play, and gives it back after. Needs [VirtualHere](https://www.virtualhere.com/), sold separately. |
|
|
|
|
## Installing from the console
|
|
|
|
Every plugin runs inside the **plugin runner**, a separate service that is off until you switch it
|
|
on. Installing a plugin while it's off succeeds and starts nothing, so do this first:
|
|
|
|
1. Open the [web console](/docs/web-console) → **Plugins** → **Installed** and look at the **Plugin
|
|
runner** card. If it says *Not installed*, install the runner package first (see
|
|
[Troubleshooting](#troubleshooting) below). If it says *Disabled*, press **Enable runner** —
|
|
once per host. *Running* means you're set; *Stopped* means the runner is enabled but not up right
|
|
now, and its log says why (see [Troubleshooting](#troubleshooting)).
|
|
2. Go to **Browse**, pick a plugin from the catalog and confirm. The host installs it and restarts
|
|
the runner, and the plugin's own page appears in the console's nav.
|
|
|
|
**Sources** is the third tab: where catalogs come from.
|
|
|
|
The rest of this page covers the CLI, which does the same thing, and the trust model behind the
|
|
badges — worth reading once, because a plugin runs with the same privileges as the host.
|
|
|
|
### What "Verified" means
|
|
|
|
Every catalogued plugin pins **one exact version** and that version's package hash. A verified entry
|
|
means somebody at unom reviewed *that exact package* — not the project in general, and not whatever
|
|
it publishes next. When a plugin releases a new version, the store keeps offering the reviewed one
|
|
until the new release is reviewed too. Before anything is downloaded, the host re-checks the pinned
|
|
hash against the registry, so a package that was quietly republished under the same version number
|
|
is refused rather than installed.
|
|
|
|
A catalog can also **revoke** a version. When an advisory covers an entry, the console shows the
|
|
reason against it and won't install that version — on **Browse** as a red-ringed panel with the
|
|
install button disabled, on **Installed** as a warning against the plugin you already have. It
|
|
never removes running code for you; that stays your decision.
|
|
|
|
Three things a plugin can be:
|
|
|
|
| Badge | Where it came from |
|
|
|---|---|
|
|
| **Verified** | The built-in catalog. unom reviewed this exact package. |
|
|
| **External source**, *from <source>* | A catalog **you** added. Still pinned and hash-checked, but curated by somebody else — unom has not looked at the code. This badge is amber, not the red **Unverified** below. |
|
|
| **Unverified** | Installed by hand from a package spec. Nobody reviewed it and nothing pins it; the console asks you to type the name to confirm, and the plugin stays marked this way for as long as it is installed. |
|
|
|
|
### Adding another catalog
|
|
|
|
**Sources** → *Add a catalog source*: a name and the URL of its index. Optionally paste the source's
|
|
`ed25519:…` public key — with a key set, the host refuses any index from that source that isn't
|
|
correctly signed, rather than falling back to an unsigned one.
|
|
|
|
Adding a source is a trust decision you make once: its plugins become installable on this host.
|
|
They are always attributed to it and never carry the Verified badge, which belongs to the built-in
|
|
catalog alone.
|
|
|
|
To publish a plugin to the built-in catalog, open a pull request against
|
|
[`punktfunk-plugin-index`](https://git.unom.io/unom/punktfunk-plugin-index) — its README covers the
|
|
format and what review looks for.
|
|
|
|
## Installing from the CLI
|
|
|
|
Two commands: install the plugin, then turn the runner on. The host CLI handles the rest — creating
|
|
the plugins directory, pointing it at the package registry, and starting the supervisor.
|
|
|
|
<Tabs items={['Linux', 'Windows']}>
|
|
<Tab value="Linux">
|
|
|
|
```sh
|
|
punktfunk-host plugins add playnite # or: rom-manager
|
|
punktfunk-host plugins enable # turn the runner on (once)
|
|
```
|
|
|
|
On **SteamOS** the [host installer](/docs/steamos-host) ships the runner automatically (user-scoped
|
|
under `~/.local` — the read-only `/usr` can't take the package). If the console reports the runner
|
|
isn't installed on an older setup, re-run `scripts/steamdeck/update.sh` once.
|
|
|
|
</Tab>
|
|
<Tab value="Windows">
|
|
|
|
Run these from an **elevated** PowerShell — right-click **PowerShell** → **Run as administrator**.
|
|
The plugins directory lives under `%ProgramData%\punktfunk`, which is admin-owned. The runner task
|
|
itself runs as the low-privilege `NT AUTHORITY\LocalService` account — `plugins enable` sets that
|
|
up (including read access to the runner's scoped API token).
|
|
|
|
```powershell
|
|
punktfunk-host plugins add playnite # or: rom-manager
|
|
punktfunk-host plugins enable # turn the runner on (once)
|
|
```
|
|
|
|
If `punktfunk-host` isn't found, open a **new** terminal (the installer adds it to `PATH`), or use
|
|
the full path: `& "$env:ProgramFiles\punktfunk\punktfunk-host.exe" plugins add playnite`.
|
|
|
|
</Tab>
|
|
</Tabs>
|
|
|
|
Open the [web console](/docs/web-console) and the plugin's page appears in the nav automatically —
|
|
that's the whole install.
|
|
|
|
The runner is **on by default** on a new install — your game sources are plugins, so a host without
|
|
it would show an empty library. (On a host that predates this, it stays however you left it; turn it
|
|
on with `punktfunk-host plugins enable`, which you only need once.) The runner discovers plugins
|
|
when it starts, so one installed later needs a restart to come up
|
|
(`systemctl --user restart punktfunk-scripting`, or `Restart` the `PunktfunkScripting` task) — the
|
|
console does that restart for you as part of installing.
|
|
|
|
Don't want it? It is a normal service you can switch off: `systemctl --user mask punktfunk-scripting`
|
|
on Linux, or disable the `PunktfunkScripting` scheduled task on Windows. Your host keeps streaming;
|
|
you just lose plugin-provided game sources and any automation.
|
|
|
|
A plugin installed from the CLI shows up in the console as **Installed via CLI**: the console knows
|
|
what is installed, but not who vouched for it. Install the same plugin from the store's Browse tab
|
|
and it carries its catalog badge instead.
|
|
|
|
### The rest of the commands
|
|
|
|
| Command | What it does |
|
|
|---|---|
|
|
| `punktfunk-host plugins add <name…>` | Install one or more plugins. |
|
|
| `punktfunk-host plugins remove <name…>` | Uninstall. |
|
|
| `punktfunk-host plugins list` | List what's installed, with versions. |
|
|
| `punktfunk-host plugins enable` | Enable + start the runner. |
|
|
| `punktfunk-host plugins disable` | Stop + disable the runner. |
|
|
| `punktfunk-host plugins status` | Is the runner enabled and running? |
|
|
|
|
A bare name resolves to the first-party package — `playnite` installs `@punktfunk/plugin-playnite`,
|
|
always from Punktfunk's own package registry. Any other name (`punktfunk-plugin-*`, a foreign
|
|
`@scope/pkg`) would install from the **public npm registry** and is refused unless you add
|
|
`--allow-public-registry` — a guard against typos and look-alike packages pulling untrusted code
|
|
onto your host.
|
|
|
|
> Plugins are operator-installed code with operator privileges — they can launch games and run
|
|
> commands. Install only plugins you trust, from a registry you control.
|
|
|
|
## Updating and removing a plugin
|
|
|
|
**Update.** The console's **Installed** tab grows an **Update to <version>** button whenever the
|
|
catalog pins a newer version than the one you have; it installs through the same confirmation as a
|
|
fresh install, and the console restarts the runner for you. There is no `plugins update` command —
|
|
from a terminal, re-run `punktfunk-host plugins add <name>`. That installs the newest version the
|
|
package registry has rather than the version the catalog pins, and it does **not** restart the
|
|
runner: restart it yourself so the new code is picked up
|
|
(`systemctl --user restart punktfunk-scripting`, or `Restart` the `PunktfunkScripting` task).
|
|
|
|
**Remove.** The **Uninstall** (bin) button on the **Installed** tab removes the package *and*
|
|
restarts the runner, so the plugin stops straight away. `punktfunk-host plugins remove <name>`
|
|
removes the package only — restart the runner yourself, as above, to stop a plugin that is still
|
|
running.
|
|
|
|
Uninstalling removes the package and nothing else. A plugin's own config and cache stay where it
|
|
wrote them — `~/.config/punktfunk/plugin-state/<plugin>/` on Linux,
|
|
`%ProgramData%\punktfunk\plugin-state\<plugin>\` on Windows — so re-installing later picks your
|
|
settings back up. Delete that directory yourself if you want it gone.
|
|
|
|
To stop *every* plugin without uninstalling anything, turn the runner off: **Disable runner** on the
|
|
Installed tab, or `punktfunk-host plugins disable`.
|
|
|
|
## ROM Manager
|
|
|
|
`@punktfunk/plugin-rom-manager` — point it at your ROM directories and it scans them, matches each
|
|
platform to an installed emulator, fetches box art (SteamGridDB, or the keyless libretro thumbnails),
|
|
and reconciles the result into your library as the `rom-manager` provider. ~25 built-in platforms
|
|
(NES through Switch, PS1/2/PSP, Dreamcast, and more), per-game overrides, and a console page to
|
|
configure it all.
|
|
|
|
```sh
|
|
punktfunk-host plugins add rom-manager
|
|
```
|
|
|
|
Then add a root or two — from the console's **ROM Manager** page, or in the config file:
|
|
|
|
<Tabs items={['Linux', 'Windows']}>
|
|
<Tab value="Linux">
|
|
|
|
`~/.config/punktfunk/plugin-state/rom-manager/config.json`:
|
|
|
|
```jsonc
|
|
{
|
|
"roots": [
|
|
{ "dir": "/mnt/roms/snes", "platform": "snes" },
|
|
{ "dir": "/mnt/roms/ps1", "platform": "ps1", "excludes": ["*.sav"] }
|
|
],
|
|
"art": { "provider": "auto", "steamGridDbKey": "" }
|
|
}
|
|
```
|
|
|
|
</Tab>
|
|
<Tab value="Windows">
|
|
|
|
`%ProgramData%\punktfunk\plugin-state\rom-manager\config.json`:
|
|
|
|
```jsonc
|
|
{
|
|
"roots": [
|
|
{ "dir": "D:\\roms\\snes", "platform": "snes" },
|
|
{ "dir": "D:\\roms\\ps1", "platform": "ps1", "excludes": ["*.sav"] }
|
|
],
|
|
"art": { "provider": "auto", "steamGridDbKey": "" }
|
|
}
|
|
```
|
|
|
|
</Tab>
|
|
</Tabs>
|
|
|
|
`plugin-state` is where a plugin reads and writes its own files — on Windows it is the one
|
|
directory `plugins enable` grants the low-privilege runner write access to. A config file placed
|
|
anywhere else under `%ProgramData%\punktfunk` is not read.
|
|
|
|
Full options and the platform/emulator list are in
|
|
[the plugin's repo](https://git.unom.io/unom/punktfunk-plugin-rom-manager).
|
|
|
|
## Playnite
|
|
|
|
`@punktfunk/plugin-playnite` — mirrors your **[Playnite](https://playnite.link)** library (Steam,
|
|
GOG, Epic, Xbox, itch, emulators, manually-added games — everything Playnite manages) into your
|
|
library. Launching a title hands it back to Playnite, which performs the real launch, so there are no
|
|
per-store launch commands to maintain. Covers are served by the host, so it scales to large libraries.
|
|
|
|
Playnite is Windows-only, so both halves of this one live on the **Windows host**. Because Playnite
|
|
keeps its library locked while running, there are **two parts**:
|
|
|
|
1. **The plugin** — from an elevated PowerShell:
|
|
|
|
```powershell
|
|
punktfunk-host plugins add playnite
|
|
punktfunk-host plugins enable
|
|
```
|
|
|
|
2. **The Punktfunk Sync extension** (in Playnite) — download `punktfunk-sync.pext` from the
|
|
[plugin's builds](https://git.unom.io/unom/punktfunk-plugin-playnite/actions) and **double-click
|
|
it** to install it in Playnite like any add-on, then restart Playnite once.
|
|
|
|
Open the console's **Playnite** page — it shows "Exporter connected", and your games sync within
|
|
seconds of any library change. Filters (installed-only, per-store, hidden) live on that page or in
|
|
`%ProgramData%\punktfunk\plugin-state\playnite\config.json`. Details are in
|
|
[the plugin's repo](https://git.unom.io/unom/punktfunk-plugin-playnite).
|
|
|
|
## VirtualHere (USB passthrough)
|
|
|
|
`@punktfunk/plugin-virtualhere` — hands a **physical USB device** on your couch machine to the
|
|
host while you play, and gives it back afterwards. The game sees the real device, so this is the
|
|
answer for a racing wheel, a HOTAS, pedals, an arcade stick, or any controller whose value is that
|
|
it is not emulated.
|
|
|
|
<Callout type="warn">
|
|
This plugin drives [VirtualHere](https://www.virtualhere.com/), a commercial USB-over-IP product
|
|
**sold separately** by VirtualHere Pty. Ltd. Nothing from VirtualHere is bundled or downloaded by
|
|
Punktfunk — you install and license it yourself. The plugin is not affiliated with or endorsed by
|
|
VirtualHere.
|
|
</Callout>
|
|
|
|
You need both halves of VirtualHere running before the plugin is any use:
|
|
|
|
- **The USB Server on the couch**, sharing the device. Free for one device; beyond that, and to run
|
|
the client as a service, VirtualHere requires a purchased licence.
|
|
- **The USB Client on the host**, ideally installed as a service so it survives logging out.
|
|
|
|
Servers exist for Windows, Linux, macOS and Android couches. **There is no VirtualHere server for
|
|
iOS or tvOS**, so iPhones, iPads and Apple TVs cannot pass devices through — nothing on the
|
|
Punktfunk side can change that.
|
|
|
|
```sh
|
|
punktfunk-host plugins add virtualhere
|
|
punktfunk-host plugins enable
|
|
```
|
|
|
|
Then open the console's **VirtualHere** page. The **Devices** tab lists whatever the couch is
|
|
sharing; pick one and it writes a rule matching the device *by name*, which keeps working after the
|
|
couch reboots or the device moves to another port. By default the device is handed over when video
|
|
starts and returned when it stops, so the couch keeps its own controller the rest of the time —
|
|
you can widen that to the whole session, or to the entire time a client is connected.
|
|
|
|
If nothing happens, the **Diagnostics** tab walks the whole two-sided setup and tells you which
|
|
part to fix. The same checks are available as `punktfunk-plugin-virtualhere doctor`, which is the
|
|
useful thing to paste into a support thread.
|
|
|
|
Full configuration is in
|
|
[the plugin's repo](https://git.unom.io/unom/punktfunk-plugin-virtualhere).
|
|
|
|
## Troubleshooting
|
|
|
|
**`punktfunk-host: command not found`** — on Windows, open a new terminal so it picks up the
|
|
installer's `PATH` change, or call the exe by full path. On Linux the host package installs it to
|
|
`/usr/bin/punktfunk-host`.
|
|
|
|
**"the plugin runner isn't installed"** — the runner ships as its own package. On Ubuntu:
|
|
`sudo apt install punktfunk-scripting`. On Fedora: `sudo dnf install punktfunk-scripting` from the
|
|
[same RPM repo you installed the host from](/docs/fedora). On Arch:
|
|
`sudo pacman -Syu punktfunk-scripting` (a full `-Syu`, like every other install from that repo).
|
|
On SteamOS, re-run `scripts/steamdeck/install.sh` (or
|
|
`scripts/steamdeck/update.sh`). On Windows, re-run the installer and keep the scripting component.
|
|
|
|
**Where a plugin's log output goes** — the console's **Logs** page, under the **Plugins** filter.
|
|
The runner ships everything your plugins print to the host, so a plugin's own lines sit next to the
|
|
host's, on one timeline, with the same search and download. Each is tagged `plugin:<name>` — the
|
|
plugin's own name for lines it logged itself, `plugin:runner` for the supervisor's (starting a
|
|
plugin, restarting a crashed one, refusing an unsafe file).
|
|
|
|
An empty Plugins view almost always means the runner isn't running — it is a separate service. Check
|
|
with `punktfunk-host plugins status`.
|
|
|
|
<Callout>
|
|
Nothing is lost if the host is down: the runner keeps buffering and sends the backlog when the host
|
|
comes back. It says so in the log if the buffer overflowed, rather than presenting a gap as
|
|
continuity.
|
|
</Callout>
|
|
|
|
**Reading the runner's log directly** — rarely needed now, but it is the ground truth if the runner
|
|
can't reach the host at all:
|
|
|
|
<Tabs items={['Linux', 'Windows']}>
|
|
<Tab value="Linux">
|
|
|
|
```sh
|
|
journalctl --user -u punktfunk-scripting -f
|
|
```
|
|
|
|
</Tab>
|
|
<Tab value="Windows">
|
|
|
|
The runner task doesn't write a log file, so run it in the foreground to watch it start your
|
|
plugins (stop it with <kbd>Ctrl</kbd>+<kbd>C</kbd>):
|
|
|
|
```powershell
|
|
& "$env:ProgramFiles\punktfunk\bun\bun.exe" "$env:ProgramFiles\punktfunk\scripting\runner-cli.js"
|
|
```
|
|
|
|
</Tab>
|
|
</Tabs>
|
|
|
|
**A plugin can't reach a service running on the same box (Linux)** — plugins that drive a local
|
|
daemon usually talk to it over a socket or FIFO in `/tmp`. The runner's unit shipped with
|
|
`PrivateTmp=yes` in earlier releases, which gave it a private `/tmp` and hid all of it: the plugin would
|
|
launch the vendor's binary happily and then time out reaching the daemon behind it, while the same
|
|
command worked perfectly in your own shell. If you are on an older host, or you have a drop-in that
|
|
reinstates it, put the real `/tmp` back:
|
|
|
|
```sh
|
|
systemctl --user edit punktfunk-scripting
|
|
```
|
|
```ini
|
|
[Service]
|
|
PrivateTmp=no
|
|
ReadWritePaths=/tmp
|
|
```
|
|
|
|
## Writing your own
|
|
|
|
A plugin is a TypeScript module built on **`@punktfunk/plugin-kit`** (`definePluginKit`), supervised
|
|
by the runner. The kit owns lifecycle, config and state, the library sync engine, and serving the
|
|
plugin's console page; `@punktfunk/host` (`definePlugin`) is the lower-level host client underneath
|
|
it. Start from the
|
|
[plugin-kit README](https://git.unom.io/unom/punktfunk/src/branch/main/plugin-kit) and
|
|
[ROM Manager](https://git.unom.io/unom/punktfunk-plugin-rom-manager), which is the reference
|
|
implementation — or any of the three plugins above.
|