e84cc89222
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 (proven on-glass). Move the plugin's state to <config_dir>/plugin-state/rom-manager, 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 — one path, no branch. Migrate a pre-0.2.1 config/cache from the old <config_dir>/rom-manager on first load (copy, guarded, best-effort) so an existing operator's roots + launch templates survive the move instead of silently resetting to defaults. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
69 lines
3.1 KiB
TypeScript
69 lines
3.1 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 `rom-manager/` subtree under it (design §9), 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/rom-manager`.
|
|
*
|
|
* 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 want to 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", "rom-manager");
|
|
|
|
/** The pre-0.2.1 location (`<config_dir>/rom-manager`), migrated away from on first run (state.ts). */
|
|
export const legacyPluginDir = (): string =>
|
|
path.join(hostConfigDir(), "rom-manager");
|
|
|
|
/** `<config_dir>/rom-manager/config.json` — operator-editable, atomic-written. */
|
|
export const configPath = (): string => path.join(pluginDir(), "config.json");
|
|
|
|
/** `<config_dir>/rom-manager/cache.json` — disposable derived state (art verdicts, detection, 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
|
|
* (`dist/index.js`) 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;
|
|
}
|
|
// Fallback to the bundled layout (dist/index.js → dist/ui) even if unbuilt.
|
|
return path.join(path.dirname(fileURLToPath(fromUrl)), "ui");
|
|
};
|