diff --git a/server/install-plugin.js b/server/install-plugin.js index c8cbb81..d6ba3c1 100644 --- a/server/install-plugin.js +++ b/server/install-plugin.js @@ -1,6 +1,46 @@ const fs = require("fs"); const path = require("path"); +// .ignis metadata helpers +async function getIgnisMeta(vaultPath) { + const ignisDir = path.join(vaultPath, ".ignis"); + const metaFile = path.join(ignisDir, "meta.json"); + + try { + const content = await fs.promises.readFile(metaFile, "utf-8"); + return JSON.parse(content); + } catch (e) { + return {}; + } +} + +async function setIgnisMeta(vaultPath, data) { + const ignisDir = path.join(vaultPath, ".ignis"); + const metaFile = path.join(ignisDir, "meta.json"); + + await fs.promises.mkdir(ignisDir, { recursive: true }); + await fs.promises.writeFile(metaFile, JSON.stringify(data, null, 2)); +} + +async function checkPluginInstalled(vaultPath) { + const pluginDir = path.join( + vaultPath, + ".obsidian", + "plugins", + "ignis-bridge", + ); + const manifestPath = path.join(pluginDir, "manifest.json"); + const mainPath = path.join(pluginDir, "main.js"); + + try { + await fs.promises.access(manifestPath); + await fs.promises.access(mainPath); + return true; + } catch (e) { + return false; + } +} + async function installPluginInVault(vaultPath) { const obsidianDir = path.join(vaultPath, ".obsidian"); const pluginDir = path.join(obsidianDir, "plugins", "ignis-bridge"); @@ -62,4 +102,10 @@ async function installPluginInAllVaults(vaultRoot) { } } -module.exports = { installPluginInVault, installPluginInAllVaults }; +module.exports = { + installPluginInVault, + installPluginInAllVaults, + getIgnisMeta, + setIgnisMeta, + checkPluginInstalled, +}; diff --git a/server/routes/vault.js b/server/routes/vault.js index 1d8503d..daf30eb 100644 --- a/server/routes/vault.js +++ b/server/routes/vault.js @@ -2,6 +2,12 @@ const express = require("express"); const fs = require("fs"); const config = require("../config"); const path = require("path"); +const { + checkPluginInstalled, + getIgnisMeta, + setIgnisMeta, + installPluginInVault, +} = require("../install-plugin"); const router = express.Router(); @@ -19,7 +25,7 @@ router.get("/list", (req, res) => { }); // GET /api/vault/info?vault= - returns info for a specific vault -router.get("/info", (req, res) => { +router.get("/info", async (req, res) => { const vaultId = req.query.vault || config.defaultVaultId; const vaultPath = config.getVaultPath(vaultId); @@ -27,12 +33,19 @@ router.get("/info", (req, res) => { return res.status(404).json({ error: "Vault not found", id: vaultId }); } + const pluginInstalled = await checkPluginInstalled(vaultPath); + const ignisMeta = await getIgnisMeta(vaultPath); + res.json({ id: vaultId, name: vaultId, path: vaultPath, platform: process.platform, version: config.obsidianVersion, + ignisPlugin: { + installed: pluginInstalled, + prompted: ignisMeta.pluginPrompted || false, + }, }); }); @@ -143,4 +156,42 @@ router.delete("/remove", async (req, res) => { } }); +// POST /api/vault/install-plugin { vault, dismiss } - install plugin or mark as prompted +router.post("/install-plugin", async (req, res) => { + const vaultId = req.body?.vault; + const dismiss = req.body?.dismiss || false; + + if (!vaultId) { + return res.status(400).json({ error: "Missing vault ID" }); + } + + const vaultPath = config.getVaultPath(vaultId); + + if (!vaultPath) { + return res.status(404).json({ error: "Vault not found" }); + } + + try { + const meta = await getIgnisMeta(vaultPath); + + if (dismiss) { + // User clicked "Don't Ask Again" or "Not Now" + meta.pluginPrompted = true; + await setIgnisMeta(vaultPath, meta); + + return res.json({ ok: true, prompted: true }); + } else { + // User wants to install the plugin + const installed = await installPluginInVault(vaultPath); + + meta.pluginPrompted = true; + await setIgnisMeta(vaultPath, meta); + + return res.json({ ok: true, installed, prompted: true }); + } + } catch (e) { + res.status(500).json({ error: e.message, code: e.code }); + } +}); + module.exports = router; diff --git a/src/services/vault-service.js b/src/services/vault-service.js index 64a0d85..0480d6b 100644 --- a/src/services/vault-service.js +++ b/src/services/vault-service.js @@ -48,6 +48,8 @@ export const vaultService = { body: JSON.stringify({ name }), }); + this._setVaultTrust(name); + return this.listVaults(); }, @@ -121,6 +123,10 @@ export const vaultService = { target.location.href = "/?vault=" + encodeURIComponent(id); }, + _setVaultTrust(vaultId, trusted = true) { + localStorage.setItem("enable-plugin-" + vaultId, String(trusted)); + }, + _migrateLocalStorage(oldId, newId) { const pluginKey = "enable-plugin-"; diff --git a/src/shims/loader.js b/src/shims/loader.js index d517d61..d80be06 100644 --- a/src/shims/loader.js +++ b/src/shims/loader.js @@ -16,6 +16,7 @@ import * as osShim from "./node/os.js"; import * as netShim from "./node/net.js"; import * as httpShim from "./node/http.js"; import { vaultService } from "../services/vault-service.js"; +import { showPluginInstallDialog } from "../ui/bootstrap.js"; const DEBUG = true; const _accessLog = new Map(); // "module.property" -> count @@ -208,6 +209,8 @@ window.__currentVaultId = path: "/", }; + window.__ignisPlugin = info.ignisPlugin || null; + console.log("[ignis] Vault:", window.__vaultConfig); console.log("[ignis] Obsidian version:", window.__obsidianVersion); } else { @@ -259,4 +262,25 @@ window.__currentVaultId = installRequestUrlShim(); +// Check if plugin install prompt is needed (once per session, after workspace loads) +if ( + window.__ignisPlugin && + !window.__ignisPlugin.installed && + !window.__ignisPlugin.prompted +) { + const vaultId = window.__currentVaultId; + + const observer = new MutationObserver(() => { + if (document.querySelector(".workspace")) { + observer.disconnect(); + showPluginInstallDialog(vaultId); + } + }); + + observer.observe(document.documentElement, { + childList: true, + subtree: true, + }); +} + console.log("[ignis] Shim loader initialized"); diff --git a/src/ui/bootstrap.js b/src/ui/bootstrap.js index 0028722..9e4345d 100644 --- a/src/ui/bootstrap.js +++ b/src/ui/bootstrap.js @@ -48,6 +48,42 @@ export function showConfirmDialog( }); } +export function showPluginInstallDialog(vaultId) { + return new Promise((resolve) => { + const dialog = new window.IgnisUI.PluginInstallDialog({ + target: document.body, + }); + + dialog.$on("install", async () => { + try { + await fetch("/api/vault/install-plugin", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ vault: vaultId }), + }); + } catch (e) { + console.error("[ignis] Failed to install plugin:", e); + } + dialog.$destroy(); + resolve("install"); + }); + + dialog.$on("dismiss", async () => { + try { + await fetch("/api/vault/install-plugin", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ vault: vaultId, dismiss: true }), + }); + } catch (e) { + console.error("[ignis] Failed to dismiss plugin prompt:", e); + } + dialog.$destroy(); + resolve("dismiss"); + }); + }); +} + export function showPromptDialog( title, label, diff --git a/src/ui/components/layout/PluginInstallDialog.svelte b/src/ui/components/layout/PluginInstallDialog.svelte new file mode 100644 index 0000000..34db792 --- /dev/null +++ b/src/ui/components/layout/PluginInstallDialog.svelte @@ -0,0 +1,89 @@ + + + + + + + +
+

This vault doesn't have the Ignis Bridge plugin installed.

+

+ The plugin adds additional functionality such as file uploads. + Obsidian will work without it, but some features will be unavailable. +

+
+ + + + +
+ + diff --git a/src/ui/index.js b/src/ui/index.js index 11fd0d4..6fdb228 100644 --- a/src/ui/index.js +++ b/src/ui/index.js @@ -2,3 +2,4 @@ export { default as VaultManager } from "./views/VaultManager.svelte"; export { default as MessageDialog } from "./components/layout/MessageDialog.svelte"; export { default as ConfirmDialog } from "./components/layout/ConfirmDialog.svelte"; export { default as PromptDialog } from "./components/layout/PromptDialog.svelte"; +export { default as PluginInstallDialog } from "./components/layout/PluginInstallDialog.svelte";