M2 of design/library-scanner-plugins-implementation-plan.md. Everything a
library scanner plugin needs is now expressible over the API; all additive.
WP2.1/2.2 — store claims (D2). library.json gains a v2 shape ({entries, claims})
that loads the v1 bare array unchanged and is written on the first mutation.
PUT /library/provider/{p}?store=<s> claims a store for a provider: its entries
then surface with deterministic <store>:<external_id> ids and the store's own
badge instead of opaque custom:<id> ones. That identity is the whole point —
entry ids, GameStream FNV app ids, client art caches and Moonlight pins all
survive a title moving from an in-host scanner to a plugin. One provider per
store (409 otherwise); DELETE releases; an empty reconcile does NOT (a store can
legitimately have zero titles). While a claim is held, all_games() skips the
matching built-in scanner, so the two never double-list during the bridge.
WP2.3 — DetectHint gains steam_appid and env_marker, the two store-derived
signals the host used to read for itself. Without them a steam plugin's lease
tracking would drop from reaper-exact to dir-prefix, and Heroic-under-Proton
would lose the only signal that works. Malformed markers are dropped, not
honoured — this feeds a path that can end processes.
WP2.4/2.5 — role: game|launcher on the entry shapes (serde-default, skipped when
default), and a steam_ui launch kind valued bigpicture|desktop that opens the
Steam client itself. Validated inbound as well as at launch.
WP2.6 — GET/PUT /library/scanners generalizes to SOURCES: built-in scanners
minus claimed ones, plus claimed stores, plus any provider with entries. The
same library-scanners.json disabled-set backs all of them and the ids match by
construction, so a user's disabled state carries over verbatim through the whole
migration. A disabled plugin source has its entries filtered at read time,
exactly like a disabled scanner.
WP2.7/2.8 — plugin registration gains a category field (the console keeps
library plugins out of the nav); index entries gain categories and per-platform
detect probes, evaluated existence-only into CatalogEntry.detected so the host
never re-grows per-store knowledge. Index SCHEMA stays 1 — additive.
WP2.9 — OpenAPI + SDK regenerated on Linux; kit wire widened (LaunchSpec.kind is
now a plain string documented against the host's vocabulary — closes G3), and
ProviderClient.reconcile takes an optional store and returns the host's echoed
entries so a caller can detect a pre-M2 host silently ignoring the claim.
Also fixes a bug the S3 spike turned up: is_steam_launch gated on a steam:// URI,
so a steam_ui launcher entry would have skipped BOTH gamescope's --steam mode and
the B1 single-instance free — on a box autologged into game mode, the nested
second Steam would see the first and exit, crashing the spawn. It now tests the
first token.
Gates on .21: workspace tests green (punktfunk-host 425 passed), workspace
clippy -D warnings clean, cargo fmt --all --check clean, OpenAPI drift test
green. plugin-kit: tsc clean, 20 tests pass.
Plugin contracts share ProviderEntry/Artwork/LaunchSpec/PrepStep with their
UIs; the kit root stays node-only (reconcile keeps the HostClient transport).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ~80% of every plugin that was copy-pasted (rom-manager ↔ playnite),
extracted as one Effect-v4 package:
- runtime: definePluginKit — async-main boundary hiding a ManagedRuntime
(two-effect-instances discipline), signal-driven interruption with a
bounded shutdown grace
- config: Schema-driven raw round-trip (defaults only in the Schema via
withDecodingDefaultKey + encodingStrategy omit; file stays authored-shape),
atomic writes, world-writable refusal, changes stream
- cache-store: disposable derived state, corrupt→empty, write-through
- reconcile: kit-owned provider wire schemas + typed client over the
skew-safe untyped pf.request seam
- sync-engine: generic poll/watch/debounce/single-flight-coalesce/
fingerprint-skip engine with a status PubSub (the SSE feed)
- ui-server + sse: effect/unstable/httpapi behind servePluginUi with
core-only env layers (validated by the phase-0 spikes); raw SSE route
(httpapi has no event-stream media type)
- cli: minimal command dispatcher reusing the plugin layer graph
(deliberately not effect/unstable/cli — it needs platform packages)
- react subpath: plugin router (path→hash→fallback deep-link restore +
pf-ui:navigate bridge), ResultGate, sseAtom, resolvePluginBase
- theme.css: the console's violet identity packaged for plugin UIs
18 bun tests incl. the two phase-0 spikes; publish workflow mirrors
sdk-publish (tag plugin-kit-v*; 0.1.0 published manually).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>