// 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: `/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 (`/rom-manager`), migrated away from on first run (state.ts). */ export const legacyPluginDir = (): string => path.join(hostConfigDir(), "rom-manager"); /** `/rom-manager/config.json` — operator-editable, atomic-written. */ export const configPath = (): string => path.join(pluginDir(), "config.json"); /** `/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"); };