// Plugin-tree maintenance for the embedded core (no Electron imports, so this // is unit-testable with plain node). // // Two problems this solves, both invisible from the gateway side: // 1. The core deliberately does NOT default `plugin_dir` (an operator opts // in). A desktop profile without it runs with ZERO plugins — the channels // plugin (account login, credential vault, auto check-in) never appears. // 2. The core seeds bundled plugins only when the file is ABSENT, so a // desktop upgrade would keep the user's old channels.lua forever and // silently skip every channel fix. On version change the shell must drop // the built-in files so the new binary reseeds them. const fs = require("fs"); const path = require("path"); const BUILTIN_PLUGINS = ["billing.lua", "channels.lua", "quota.lua", "usage.lua"]; // ensurePluginDirConfigured back-fills `plugin_dir` into an existing config. // Appending a top-level YAML key is safe for the shapes this file can have // (flat mapping + a keys list). Returns "added" | "present" | "absent" | "error". function ensurePluginDirConfigured(configFile, pluginsDir, log) { try { if (!fs.existsSync(configFile)) return "absent"; const cur = fs.readFileSync(configFile, "utf-8"); if (/^\s*plugin_dir\s*:/m.test(cur)) return "present"; fs.writeFileSync( configFile, cur.replace(/\s*$/, "") + "\n# added by ModelRouter Desktop: enables the plugin tree (channels etc.)\n" + "plugin_dir: " + JSON.stringify(pluginsDir) + "\n", "utf-8", ); if (log) log("[gui] plugin_dir back-filled into " + configFile); return "added"; } catch (e) { if (log) log("[gui] ensurePluginDirConfigured failed: " + e.message); return "error"; } } // syncBundledPluginsOnUpgrade drops built-in plugin files when the app version // changed, so the core reseeds them from the binary it shipped with. Returns // { changed, dropped:[...], kept: "prev-" }. function syncBundledPluginsOnUpgrade(profileDir, pluginsDir, version, log) { const stamp = path.join(profileDir, ".plugin-seed-version"); let have = ""; try { have = fs.readFileSync(stamp, "utf-8").trim(); } catch (e) { /* first run */ } if (have === version) return { changed: false, dropped: [] }; const dropped = []; for (const name of BUILTIN_PLUGINS) { const f = path.join(pluginsDir, name); if (!fs.existsSync(f)) continue; try { if (have) fs.renameSync(f, f + ".prev-" + have); else fs.unlinkSync(f); dropped.push(name); } catch (e) { if (log) log("[gui] drop builtin plugin " + name + " failed: " + e.message); } } try { fs.writeFileSync(stamp, version + "\n", "utf-8"); } catch (e) { if (log) log("[gui] write plugin seed stamp failed: " + e.message); } if (dropped.length && log) { log( "[gui] plugins refreshed for " + version + " (" + dropped.length + " builtin file(s) reseeded by core)", ); } return { changed: true, dropped, kept: have ? "prev-" + have : "" }; } module.exports = { BUILTIN_PLUGINS, ensurePluginDirConfigured, syncBundledPluginsOnUpgrade, };