diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..de42adf --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": false, + "tabWidth": 2, + "trailingComma": "all", + "printWidth": 80 +} diff --git a/server/bridge-plugin.js b/server/bridge-plugin.js new file mode 100644 index 0000000..9543508 --- /dev/null +++ b/server/bridge-plugin.js @@ -0,0 +1,70 @@ +const fs = require("fs"); +const path = require("path"); +const { + installObsidianPlugin, + isObsidianPluginInstalled, +} = require("./plugin-system/obsidian-plugin"); + +const BRIDGE_PLUGIN_ID = "ignis-bridge"; +const BRIDGE_PLUGIN_DIR = path.join(__dirname, "..", "plugin"); + +// .ignis metadata helpers + +async function getIgnisMeta(vaultPath) { + const metaFile = path.join(vaultPath, ".ignis", "meta.json"); + + try { + const content = await fs.promises.readFile(metaFile, "utf-8"); + return JSON.parse(content); + } catch { + 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)); +} + +// Bridge plugin install/check + +async function isBridgePluginInstalled(vaultPath) { + return isObsidianPluginInstalled(BRIDGE_PLUGIN_ID, vaultPath); +} + +async function installBridgePlugin(vaultPath) { + const result = await installObsidianPlugin(BRIDGE_PLUGIN_DIR, vaultPath); + return result.installed; +} + +async function updateBridgePluginInAllVaults(vaultRoot) { + if (!(await fs.promises.stat(vaultRoot).catch(() => null))) { + return; + } + + const entries = await fs.promises.readdir(vaultRoot, { withFileTypes: true }); + + for (const entry of entries) { + if (!entry.isDirectory()) { + continue; + } + + const vaultPath = path.join(vaultRoot, entry.name); + const installed = await installBridgePlugin(vaultPath); + + if (installed) { + console.log(`[ignis] Installed bridge plugin in vault: ${entry.name}`); + } + } +} + +module.exports = { + installBridgePlugin, + updateBridgePluginInAllVaults, + isBridgePluginInstalled, + getIgnisMeta, + setIgnisMeta, +}; diff --git a/server/index.js b/server/index.js index 42072a0..dc406b7 100644 --- a/server/index.js +++ b/server/index.js @@ -3,7 +3,8 @@ const path = require("path"); const compression = require("compression"); const config = require("./config"); const { setupWebSocket } = require("./ws"); -const { installPluginInAllVaults } = require("./install-plugin"); +const watcher = require("./watcher"); +const { updateBridgePluginInAllVaults } = require("./bridge-plugin"); const ANSI_RED = "\x1b[31m"; const ANSI_YELLOW = "\x1b[33m"; @@ -97,7 +98,26 @@ const server = app.listen(config.port, async () => { console.log(`[ignis] Vault root: ${config.vaultRoot}`); console.log(`[ignis] Vaults: ${Object.keys(config.vaults).join(", ")}`); - await installPluginInAllVaults(config.vaultRoot); + await updateBridgePluginInAllVaults(config.vaultRoot); }); -setupWebSocket(server); +const wss = setupWebSocket(server); + +async function gracefulShutdown(signal) { + console.log(`\n[ignis] Received ${signal}, shutting down gracefully...`); + + await shutdownPlugins(); + + server.close(() => { + console.log("[ignis] Server closed"); + process.exit(0); + }); + + setTimeout(() => { + console.error("[ignis] Forced shutdown after timeout"); + process.exit(1); + }, 10000); +} + +process.on("SIGTERM", () => gracefulShutdown("SIGTERM")); +process.on("SIGINT", () => gracefulShutdown("SIGINT")); diff --git a/server/install-plugin.js b/server/install-plugin.js deleted file mode 100644 index 6d5a583..0000000 --- a/server/install-plugin.js +++ /dev/null @@ -1,109 +0,0 @@ -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"); - - if (!(await fs.promises.stat(obsidianDir).catch(() => null))) { - return false; - } - - await fs.promises.mkdir(pluginDir, { recursive: true }); - - const pluginSrcDir = path.join(__dirname, "..", "plugin"); - await fs.promises.copyFile( - path.join(pluginSrcDir, "manifest.json"), - path.join(pluginDir, "manifest.json"), - ); - await fs.promises.copyFile( - path.join(pluginSrcDir, "main.js"), - path.join(pluginDir, "main.js"), - ); - - const pluginsConfig = path.join(obsidianDir, "community-plugins.json"); - let plugins = []; - - if (await fs.promises.stat(pluginsConfig).catch(() => null)) { - try { - plugins = JSON.parse(await fs.promises.readFile(pluginsConfig, "utf8")); - } catch (e) { - plugins = []; - } - } - - if (!plugins.includes("ignis-bridge")) { - plugins.push("ignis-bridge"); - await fs.promises.writeFile(pluginsConfig, JSON.stringify(plugins)); - return true; - } - - return false; -} - -async function installPluginInAllVaults(vaultRoot) { - if (!(await fs.promises.stat(vaultRoot).catch(() => null))) { - return; - } - - const entries = await fs.promises.readdir(vaultRoot, { withFileTypes: true }); - - for (const entry of entries) { - if (entry.isDirectory()) { - const vaultPath = path.join(vaultRoot, entry.name); - const installed = await installPluginInVault(vaultPath); - - if (installed) { - console.log(`[ignis] Installed plugin in vault: ${entry.name}`); - } - } - } -} - -module.exports = { - installPluginInVault, - installPluginInAllVaults, - getIgnisMeta, - setIgnisMeta, - checkPluginInstalled, -}; diff --git a/server/plugin-system/obsidian-plugin.js b/server/plugin-system/obsidian-plugin.js new file mode 100644 index 0000000..083ea03 --- /dev/null +++ b/server/plugin-system/obsidian-plugin.js @@ -0,0 +1,110 @@ +const fs = require("fs"); +const path = require("path"); + +async function readManifestId(sourceDir) { + const manifestPath = path.join(sourceDir, "manifest.json"); + const content = await fs.promises.readFile(manifestPath, "utf-8"); + const manifest = JSON.parse(content); + + if (!manifest.id) { + throw new Error(`No "id" in manifest.json at ${sourceDir}`); + } + + return manifest.id; +} + +async function installObsidianPlugin(sourceDir, vaultPath) { + const pluginId = await readManifestId(sourceDir); + + const obsidianDir = path.join(vaultPath, ".obsidian"); + + try { + await fs.promises.access(obsidianDir); + } catch { + return { installed: false, pluginId }; + } + + const targetDir = path.join(obsidianDir, "plugins", pluginId); + await fs.promises.mkdir(targetDir, { recursive: true }); + + const files = await fs.promises.readdir(sourceDir); + + for (const file of files) { + const srcPath = path.join(sourceDir, file); + const stat = await fs.promises.stat(srcPath); + + if (stat.isFile()) { + await fs.promises.copyFile(srcPath, path.join(targetDir, file)); + } + } + + const pluginsConfigFile = path.join(obsidianDir, "community-plugins.json"); + let plugins = []; + + try { + const content = await fs.promises.readFile(pluginsConfigFile, "utf-8"); + plugins = JSON.parse(content); + } catch { + plugins = []; + } + + if (!plugins.includes(pluginId)) { + plugins.push(pluginId); + await fs.promises.writeFile(pluginsConfigFile, JSON.stringify(plugins)); + } + + return { installed: true, pluginId }; +} + +async function removeObsidianPlugin(sourceDir, vaultPath) { + const pluginId = await readManifestId(sourceDir); + + const obsidianDir = path.join(vaultPath, ".obsidian"); + + try { + await fs.promises.access(obsidianDir); + } catch { + return { removed: false, pluginId }; + } + + const targetDir = path.join(obsidianDir, "plugins", pluginId); + + try { + await fs.promises.rm(targetDir, { recursive: true }); + } catch { + // Already gone + } + + const pluginsConfigFile = path.join(obsidianDir, "community-plugins.json"); + + try { + const content = await fs.promises.readFile(pluginsConfigFile, "utf-8"); + let plugins = JSON.parse(content); + plugins = plugins.filter((id) => id !== pluginId); + await fs.promises.writeFile(pluginsConfigFile, JSON.stringify(plugins)); + } catch { + // No config file or parse error - nothing to remove from + } + + return { removed: true, pluginId }; +} + +async function isObsidianPluginInstalled(pluginId, vaultPath) { + const pluginDir = path.join(vaultPath, ".obsidian", "plugins", pluginId); + 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 { + return false; + } +} + +module.exports = { + installObsidianPlugin, + removeObsidianPlugin, + isObsidianPluginInstalled, +}; diff --git a/server/routes/vault.js b/server/routes/vault.js index daf30eb..ac42e97 100644 --- a/server/routes/vault.js +++ b/server/routes/vault.js @@ -3,11 +3,11 @@ const fs = require("fs"); const config = require("../config"); const path = require("path"); const { - checkPluginInstalled, + isBridgePluginInstalled, getIgnisMeta, setIgnisMeta, - installPluginInVault, -} = require("../install-plugin"); + installBridgePlugin, +} = require("../bridge-plugin"); const router = express.Router(); @@ -33,7 +33,7 @@ router.get("/info", async (req, res) => { return res.status(404).json({ error: "Vault not found", id: vaultId }); } - const pluginInstalled = await checkPluginInstalled(vaultPath); + const pluginInstalled = await isBridgePluginInstalled(vaultPath); const ignisMeta = await getIgnisMeta(vaultPath); res.json({ @@ -65,30 +65,7 @@ router.post("/create", async (req, res) => { recursive: false, }); - // Install ignis-bridge plugin - const pluginDir = path.join( - vaultPath, - ".obsidian", - "plugins", - "ignis-bridge", - ); - await fs.promises.mkdir(pluginDir, { recursive: true }); - - const pluginSrcDir = path.join(__dirname, "..", "..", "plugin"); - await fs.promises.copyFile( - path.join(pluginSrcDir, "manifest.json"), - path.join(pluginDir, "manifest.json"), - ); - await fs.promises.copyFile( - path.join(pluginSrcDir, "main.js"), - path.join(pluginDir, "main.js"), - ); - - // Enable the plugin - await fs.promises.writeFile( - path.join(vaultPath, ".obsidian", "community-plugins.json"), - JSON.stringify(["ignis-bridge"]), - ); + await installBridgePlugin(vaultPath); config.refreshVaults(); @@ -182,7 +159,7 @@ router.post("/install-plugin", async (req, res) => { return res.json({ ok: true, prompted: true }); } else { // User wants to install the plugin - const installed = await installPluginInVault(vaultPath); + const installed = await installBridgePlugin(vaultPath); meta.pluginPrompted = true; await setIgnisMeta(vaultPath, meta);