Files
punktfunk-plugin-playnite/src/paths.ts
T
enricobuehler 1eb0f650ab
CI / exporter (push) Successful in 12s
CI / build (push) Successful in 23s
CI / publish (push) Successful in 19s
fix: persist state under plugin-state/ (runner de-privilege)
The managed runner is de-privileged on Windows now (runs as LocalService),
and %ProgramData%\punktfunk is locked read-only to it — so writing config/
cache straight under the config dir fails EPERM. Move the plugin's state to
<config_dir>/plugin-state/playnite, which `punktfunk-host plugins enable`
grants the runner write on. On Linux the runner owns the config dir, so the
path is writable there too. First release, so no migration needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 00:15:14 +02:00

64 lines
2.8 KiB
TypeScript

// Where the plugin's own files live. The host config-dir resolution mirrors the SDK's `configDir`
// (`@punktfunk/host` does not re-export it) so we always land in the same place the host and runner
// use; the plugin owns a `plugin-state/playnite/` subtree under it, created 0700.
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
/** The host's config dir — the same resolution the host and SDK use. */
export const hostConfigDir = (): string => {
const explicit = process.env.PUNKTFUNK_CONFIG_DIR;
if (explicit) return explicit;
if (process.platform === "win32") {
const base = process.env.ProgramData ?? process.env.APPDATA ?? ".";
return path.join(base, "punktfunk");
}
const base =
process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
return path.join(base, "punktfunk");
};
/**
* This plugin's private directory: `<config_dir>/plugin-state/playnite`.
*
* The state lives under `plugin-state/`, not straight under the config dir, because the managed
* runner is de-privileged on Windows (`NT AUTHORITY\LocalService`) and the config dir is locked
* read-only there — `punktfunk-host plugins enable` grants the runner write on exactly
* `plugin-state`. (Mirrors `@punktfunk/host`'s `pluginStateDir`; reimplemented locally like
* `hostConfigDir`, since we don't gate on an SDK version bump.) On Linux the runner owns the config
* dir, so the same path is writable with no special step.
*/
export const pluginDir = (): string =>
path.join(hostConfigDir(), "plugin-state", "playnite");
/** `<config_dir>/playnite/config.json` — operator-editable, atomic-written. */
export const configPath = (): string => path.join(pluginDir(), "config.json");
/** `<config_dir>/playnite/cache.json` — disposable derived state (last reconcile fingerprint). */
export const cachePath = (): string => path.join(pluginDir(), "cache.json");
/**
* Locate the built SPA directory (`dist/ui`) at runtime. The package ships as ONE self-contained
* bundle so it installs from the Gitea registry with no external deps — which means `import.meta.url`
* can be `dist/index.js` (bundled) or `dist/server/index.js` (unbundled dev). We walk up from the
* calling module looking for a `ui/index.html`, so both layouts resolve. `fromUrl` is the caller's
* `import.meta.url`.
*/
export const resolveUiDir = (fromUrl: string): string => {
let dir = path.dirname(fileURLToPath(fromUrl));
for (let i = 0; i < 6; i++) {
for (const cand of [path.join(dir, "ui"), path.join(dir, "dist", "ui")]) {
try {
if (fs.existsSync(path.join(cand, "index.html"))) return cand;
} catch {
// keep walking
}
}
const parent = path.dirname(dir);
if (parent === dir) break;
dir = parent;
}
return path.join(path.dirname(fileURLToPath(fromUrl)), "ui");
};