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>
46 lines
1.6 KiB
TypeScript
46 lines
1.6 KiB
TypeScript
// Filesystem seam: the SDK owns the canonical path layout (plugin-state, ingest); the kit
|
|
// re-exports those and adds the two write primitives every plugin needs — a 0700 state dir
|
|
// and atomic (temp + rename, 0600) file writes. All IO is node:fs, wrapped in Effect.
|
|
import * as fs from "node:fs";
|
|
import * as path from "node:path";
|
|
import { pluginIngestDir, pluginStateDir } from "@punktfunk/host";
|
|
import { Effect } from "effect";
|
|
import { ConfigWriteError } from "./errors.js";
|
|
|
|
export { pluginIngestDir, pluginStateDir };
|
|
|
|
/** Create `pluginStateDir(name)` 0700 if absent (idempotent). Returns the dir. */
|
|
export const ensureStateDir = (
|
|
name: string,
|
|
): Effect.Effect<string, ConfigWriteError> =>
|
|
Effect.try({
|
|
try: () => {
|
|
const dir = pluginStateDir(name);
|
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
return dir;
|
|
},
|
|
catch: (cause) =>
|
|
new ConfigWriteError({ path: pluginStateDir(name), cause }),
|
|
});
|
|
|
|
/**
|
|
* Atomic write: temp file (0600) in the same dir, then rename — a crash never leaves a
|
|
* torn file, and readers only ever see complete content.
|
|
*/
|
|
export const atomicWriteFile = (
|
|
file: string,
|
|
data: string,
|
|
): Effect.Effect<void, ConfigWriteError> =>
|
|
Effect.try({
|
|
try: () => {
|
|
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
fs.writeFileSync(tmp, data, { mode: 0o600 });
|
|
fs.renameSync(tmp, file);
|
|
},
|
|
catch: (cause) => new ConfigWriteError({ path: file, cause }),
|
|
});
|
|
|
|
/** Join inside a plugin's state dir. */
|
|
export const statePath = (name: string, file: string): string =>
|
|
path.join(pluginStateDir(name), file);
|