diff --git a/Makefile b/Makefile index ca69504..9940bed 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all build test gui gui-dev gui-dist gui-deb gui-win gui-win-docker help clean +.PHONY: all build test gui gui-dev gui-dist gui-deb gui-win gui-win-docker help clean gui-test # ModelRouter - single-binary gateway + optional Electron desktop GUI (embedded core) export BINARY @@ -65,6 +65,13 @@ gui-dist-dir: test: go test ./... +# Desktop shell unit tests (no Electron needed): plugin-tree seeding/migration. +# These guard the two things that silently break the desktop build: +# - plugin_dir missing from an embedded profile -> channels plugin invisible +# - bundled plugin files never refreshed -> old channels.lua survives upgrades +gui-test: + @cd $(GUI_DIR) && node --test plugin-sync.test.js + clean: rm -rf $(BUILD_DIR) cmd/build cmd/gui/dist cmd/gui/build/gui-dist cmd/gui/bin @@ -75,6 +82,7 @@ help: @echo " make gui run desktop GUI (dev)" @echo " make gui-dev run GUI with devtools" @echo " make gui-dist build deb + rpm + AppImage (linux)" + @echo " make gui-test desktop shell unit tests (plugin tree seeding)" @echo " make gui-deb build deb only" @echo " make gui-win build Windows nsis (needs host mingw + wine)" @echo " make gui-win-docker build Windows nsis all-in-docker (no host mingw needed)" diff --git a/cmd/gui/main.js b/cmd/gui/main.js index abc6ea9..94dc370 100644 --- a/cmd/gui/main.js +++ b/cmd/gui/main.js @@ -18,6 +18,7 @@ const http = require("http"); const os = require("os"); const crypto = require("crypto"); const { spawn, execFileSync } = require("child_process"); +const pluginSync = require("./plugin-sync"); const LOG_FILE = path.join(app.getPath("userData"), "gui.log"); function log(msg) { @@ -40,6 +41,13 @@ console.error = function () { // Per-user profile dir: /profile const PROFILE_DIR = path.join(app.getPath("userData"), "profile"); const CONFIG_FILE = path.join(PROFILE_DIR, "config.yaml"); +// Plugin tree for the embedded core. The core deliberately does NOT default +// plugin_dir (an operator opts in), so the desktop build must set it — without +// it the whole plugin system stays off and the channels plugin (multi-platform +// account login + credential vault + auto check-in) is invisible in the WebUI. +// Seeding is the core's job (SeedBundled fills MISSING files only), so a user's +// edited copy always wins. +const PLUGINS_DIR = path.join(PROFILE_DIR, "plugins"); // Packaged: extraResource at process.resourcesPath/bin (outside asar, spawnable). // Dev: cmd/gui/bin, populated by prepare-core.js. const CORE_EXE = path.join( @@ -216,6 +224,9 @@ function writeDefaultEmbeddedConfig() { "listen: 127.0.0.1:" + (settings.port || DEFAULT_PORT), "default_model: AUTO", "adapter_dir: " + JSON.stringify(path.join(PROFILE_DIR, "adapters")), + // Plugin tree: channels/billing/quota/usage ship inside the core binary and + // are seeded into this dir on first start. Omitting it disables plugins. + "plugin_dir: " + JSON.stringify(PLUGINS_DIR), "runtime_file: " + JSON.stringify(path.join(PROFILE_DIR, "runtime.json")), "", // Admin key as a plain `keys` entry (no seed flag) so the web UI never @@ -268,6 +279,9 @@ function startCore() { // ensure profile dir + config + local adapters exist fs.mkdirSync(PROFILE_DIR, { recursive: true }); if (!fs.existsSync(CONFIG_FILE)) writeDefaultEmbeddedConfig(); + pluginSync.ensurePluginDirConfigured(CONFIG_FILE, PLUGINS_DIR, log); + fs.mkdirSync(PLUGINS_DIR, { recursive: true }); + pluginSync.syncBundledPluginsOnUpgrade(PROFILE_DIR, PLUGINS_DIR, app.getVersion(), log); coreProc = spawn(CORE_EXE, ["-config", CONFIG_FILE], { cwd: PROFILE_DIR, @@ -622,10 +636,55 @@ function createWindow() { }, }); mainWindow.loadFile(path.join(__dirname, "renderer", "index.html")); + // External links ALWAYS go to the user's default browser, never an in-app + // window. Three paths have to be covered, and missing any one of them is why + // "click the login link and nothing happens / the app navigates away" shows up: + // 1. window.open / target=_blank -> setWindowOpenHandler + // 2. top-level navigation (same-tab click) -> will-navigate + // 3. navigation inside the WebUI