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
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>
61 lines
3.3 KiB
Markdown
61 lines
3.3 KiB
Markdown
# @punktfunk/plugin-kit
|
|
|
|
The Effect-based framework punktfunk plugins are built on. It owns everything that is the
|
|
same in every plugin — lifecycle, config/state, the sync engine, UI serving, the CLI
|
|
scaffold, logging — so a plugin is just its domain logic, its HttpApi contract, and its UI.
|
|
The reference consumer (and the blueprint to copy) is
|
|
[`punktfunk-plugin-rom-manager`](https://git.unom.io/unom/punktfunk-plugin-rom-manager).
|
|
|
|
Built on [`@punktfunk/host`](../sdk) (the SDK stays the low-level host client; the kit is
|
|
the opinionated plugin layer on top). Effect `4.x` and the SDK are peer dependencies —
|
|
the plugin's own copies are the only copies.
|
|
|
|
## The one rule: async at the boundary, Effect inside
|
|
|
|
The packaged runner bundles its own effect + SDK; a plugin's imports resolve to the
|
|
plugin's node_modules. Effect values must therefore never cross the plugin boundary
|
|
(`Context.Tag` identity is per-instance). `definePluginKit` enforces this by construction:
|
|
you write Effect, it exports a plain async-`main` `PluginDef`, and a `ManagedRuntime`
|
|
built from *your* effect instance runs everything. SIGINT/SIGTERM interrupt the plugin
|
|
fiber (scoped finalizers run: UI deregistration, watcher close), bounded by
|
|
`shutdownGraceMs`.
|
|
|
|
```ts
|
|
import { definePluginKit, serveUi } from "@punktfunk/plugin-kit";
|
|
import { Effect, Layer } from "effect";
|
|
|
|
export default definePluginKit({
|
|
name: "my-plugin",
|
|
version: "0.1.0",
|
|
layer: MyServices.layer, // over the kit base: HostClient | PluginInfo
|
|
main: Effect.gen(function* () {
|
|
const engine = yield* MySync;
|
|
yield* engine.start;
|
|
yield* serveUi({ title: "My Plugin", icon: "puzzle", staticDir, api: MyApiLive });
|
|
yield* Effect.never;
|
|
}),
|
|
});
|
|
```
|
|
|
|
## Modules
|
|
|
|
| Export | What it owns |
|
|
| --- | --- |
|
|
| `definePluginKit` / `runPluginKitDirect` | the async-main boundary + ManagedRuntime + signal handling |
|
|
| `HostClient`, `PluginInfo` | the `pf` facade as services (`request` = the skew-safe untyped seam) |
|
|
| `makeConfigService` | Schema-driven config: raw shape on disk, defaults ONLY in the Schema (`withDecodingDefaultKey` + `encodingStrategy: "omit"`), atomic writes, world-writable refusal, `changes` stream |
|
|
| `makeCacheStore` | disposable derived state (corrupt/absent → empty, write-through) |
|
|
| `ProviderClient` + wire schemas | typed library-provider reconcile over the untyped wire |
|
|
| `makeSyncEngine` | poll + fs-watch + debounce + single-flight coalescing + fingerprint skip + status feed |
|
|
| `serveUi` / `httpApiEnv` | an `effect/unstable/httpapi` HttpApi behind the SDK's `servePluginUi`, core-only layers |
|
|
| `sseRoute` | the status SSE endpoint (httpapi has no event-stream media type) |
|
|
| `runPluginCli` | `<bin> <command>` dispatcher reusing the plugin's layer graph (deliberately not `effect/unstable/cli` — that would drag platform packages into every plugin) |
|
|
| `loggingLayer` | runner-journal line format |
|
|
| `@punktfunk/plugin-kit/react` | browser glue: `createPluginRouter` (path→hash→fallback deep-link restore + `pf-ui:navigate`), `resolvePluginBase`, `useIsEmbedded`, `ResultGate`, `sseAtom` |
|
|
| `@punktfunk/plugin-kit/theme.css` | the console's violet identity for plugin UIs (import first in your Tailwind entry) |
|
|
|
|
## Publishing
|
|
|
|
Tag `plugin-kit-vX.Y.Z` (matching `package.json`) — `.gitea/workflows/plugin-kit-publish.yml`
|
|
typechecks, tests, builds, and publishes to the Gitea registry.
|