Files
enricobuehler b81e03685a refactor: rewrite on @punktfunk/plugin-kit — contract + plugin workspaces
The framework half of this plugin (engine lifecycle, config/state, host
reconcile, UI server, CLI) was ~80% identical to rom-manager's. That code is
now @punktfunk/plugin-kit; this replaces the local copy with it and adopts the
rom-manager blueprint layout.

  contract/  Effect Schemas: the cross-process export contract (the C#
             exporter's other half, with the schema-version guard as a decode
             check), the config schema (one schema, Encoded = authored file
             shape, defaults only via withDecodingDefaultKey), domain DTOs, the
             HttpApi contract, API errors.
  plugin/    src/domain = the pure core (locate/read, art, reconcile), ported
             tests green before any Effect wiring; src/services = the kit's
             ConfigService/CacheStore/SyncEngine/ProviderClient; definePluginKit
             entry, kit CLI, auth-free dev API.

Preserved, because they are what makes playnite work at all: the ingest inbox
is still read FIRST (the de-privileged LocalService runner cannot reach the
interactive user's %APPDATA%), the host/dataurl/off art modes, and the
playnite:// launch hand-back. The C# exporter is untouched.

New: per-game include/exclude overrides, and an allow-listed /api/art proxy so
the SPA can render local Playnite covers — it serves only paths the currently
ingested export references.

Dropped: the standalone password UI server (console surface + CLI only, as in
rom-manager 0.3.0) and the devEntry flag.

Trap found and fixed here: the HttpApi encoder serialises `undefined` as
`null`, so a `Schema.optional` field the server omits cannot be decoded by the
client — silently in the SSE feed, loudly in the typed client. Every optional
EngineStatus field is `Schema.NullOr` with an explicit value on the wire.
2026-07-20 20:56:12 +02:00

47 lines
1.6 KiB
TypeScript

// The plugin entry — the default export the runner discovers. Everything is Effect inside
// definePluginKit's async-main boundary (see @punktfunk/plugin-kit): start the sync engine,
// serve the console UI, park until interruption.
//
// Provider entries are deliberately LEFT in the host library on shutdown, so the games
// survive plugin restarts and host reboots; a clean uninstall is the explicit CLI command.
import { definePluginKit, serveUi } from "@punktfunk/plugin-kit";
import { Effect } from "effect";
import pkg from "../package.json" with { type: "json" };
import { PLUGIN_NAME, UI_ICON, UI_TITLE } from "./const.js";
import { playniteLayer } from "./layer.js";
import { makeApi } from "./services/api.js";
import { PlayniteConfig } from "./services/config.js";
import { PlayniteSync } from "./services/engine.js";
export { PLUGIN_NAME, PROVIDER_ID, UI_ICON, UI_TITLE } from "./const.js";
const plugin = definePluginKit({
name: PLUGIN_NAME,
version: pkg.version,
layer: playniteLayer,
main: Effect.gen(function* () {
const sync = yield* PlayniteSync;
const config = yield* PlayniteConfig;
// Headless sync first — the library reconciles even if the UI can't come up.
yield* sync.engine.start;
yield* serveUi({
title: UI_TITLE,
icon: UI_ICON,
staticDir: new URL("./ui/", import.meta.url),
api: makeApi({ sync, config }),
}).pipe(
Effect.catch((e) =>
Effect.logWarning(
`ui: could not serve the console surface (${e.cause}) — engine continues headless`,
),
),
);
yield* Effect.never;
}),
});
export default plugin;