Files
punktfunk/plugin-kit/src/host-client.ts
T
enricobuehlerandClaude Fable 5 8d72e1d27e
ci / rust (push) Has been cancelled
android / android (push) Has been cancelled
apple / swift (push) Has been cancelled
apple / screenshots (push) Has been cancelled
arch / build-publish (push) Has been cancelled
ci / web (push) Has been cancelled
ci / bench (push) Has been cancelled
ci / docs-site (push) Has been cancelled
deb / build-publish-host (push) Has been cancelled
deb / build-publish (push) Has been cancelled
decky / build-publish (push) Has been cancelled
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
windows-host / package (push) Has been cancelled
feat(plugin-kit): @punktfunk/plugin-kit 0.1.0 — the Effect plugin framework
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>
2026-07-20 18:41:11 +02:00

54 lines
1.9 KiB
TypeScript

// The pf seam as Effect services. `HostClient.request` keeps the SDK's deliberately
// untyped `pf.request` wire (version-skew-safe against the runner-bundled SDK — design D7);
// typing happens one level up (reconcile.ts owns the wire schemas). Only the plain `pf`
// object crosses the plugin boundary, so no Effect values are shared with the runner's
// bundled effect instance.
import type { Punktfunk } from "@punktfunk/host";
import { Context, Effect, Layer } from "effect";
import { HostRequestError } from "./errors.js";
export interface HostClientService {
/** Raw management-API call (loopback + bearer, via the SDK facade). */
readonly request: (
method: string,
path: string,
body?: unknown,
) => Effect.Effect<unknown, HostRequestError>;
/**
* Escape hatch for SDK helpers that take the facade directly (`servePluginUi`).
* Never share this with code that could leak Effect values back to the runner.
*/
readonly facade: Punktfunk;
}
export class HostClient extends Context.Service<HostClient, HostClientService>()(
"@punktfunk/plugin-kit/HostClient",
) {}
/** Wrap the facade the runner hands to `main` (or `connect()` in the CLI/dev paths). */
export const hostClientFromFacade = (
pf: Punktfunk,
): Layer.Layer<HostClient> =>
Layer.succeed(HostClient)({
request: (method, path, body) =>
Effect.tryPromise({
try: () => pf.request(method, path, body),
catch: (cause) => new HostRequestError({ method, path, cause }),
}),
facade: pf,
});
export interface PluginInfoService {
/** The plugin id (`definePlugin` name, `[a-z][a-z0-9-]*`). */
readonly name: string;
readonly version?: string;
}
export class PluginInfo extends Context.Service<PluginInfo, PluginInfoService>()(
"@punktfunk/plugin-kit/PluginInfo",
) {}
export const pluginInfoLayer = (
info: PluginInfoService,
): Layer.Layer<PluginInfo> => Layer.succeed(PluginInfo)(info);