From 2f14e8c1fa260e71dbdbd277aa1ce5ef8c804fd8 Mon Sep 17 00:00:00 2001 From: Florian Egger Date: Sun, 31 May 2026 17:17:51 +0200 Subject: [PATCH] Add plugin installation script and documentation The new `install-plugin` script automates plugin installation to Obsidian vaults with support for both vault paths and direct plugin directory targets. It includes options for skipping builds and dry runs. Documentation in IMPLEMENTATION.md now includes usage examples for the new installation method alongside the existing manual installation instructions. --- IMPLEMENTATION.md | 6 ++ package.json | 1 + scripts/install.mjs | 133 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 scripts/install.mjs diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index 5d5e03a..be36ccd 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -265,6 +265,12 @@ Run `npm run build`, then copy the plugin folder containing `manifest.json` and - Linux: ~/.config/obsidian/plugins/ - macOS: ~/Library/Application Support/obsidian/plugins/ - Windows: %APPDATA%\obsidian\plugins\ + +# Or install directly into a vault +npm run install-plugin -- --vault /path/to/vault + +# Exact target directory also works +npm run install-plugin -- --plugin-dir /path/to/vault/.obsidian/plugins/obsidian-remarkable ``` --- diff --git a/package.json b/package.json index afd5347..055b8aa 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "scripts": { "build": "node esbuild.config.mjs", "dev": "node esbuild.config.mjs --watch", + "install-plugin": "node scripts/install.mjs", "test": "node tests/run-tests.mjs" }, "devDependencies": { diff --git a/scripts/install.mjs b/scripts/install.mjs new file mode 100644 index 0000000..2f4ca03 --- /dev/null +++ b/scripts/install.mjs @@ -0,0 +1,133 @@ +import { copyFile, mkdir, readFile } from "fs/promises"; +import { dirname, join, resolve } from "path"; +import { fileURLToPath } from "url"; +import { spawnSync } from "child_process"; + +const root = dirname(dirname(fileURLToPath(import.meta.url))); +const runtimeFiles = ["manifest.json", "main.js"]; + +function printUsage() { + console.log(`Usage: + npm run install-plugin -- --vault /path/to/vault + npm run install-plugin -- --plugin-dir /path/to/vault/.obsidian/plugins/obsidian-remarkable + +Options: + --vault Obsidian vault path. Installs to /.obsidian/plugins/ + --plugin-dir Exact plugin install directory + --no-build Skip npm run build before copying files + --dry-run Print actions without writing files + --help Show this help + +Environment: + OBSIDIAN_VAULT Same as --vault + OBSIDIAN_PLUGIN_DIR Same as --plugin-dir`); +} + +function parseArgs(argv) { + const options = { + vault: process.env.OBSIDIAN_VAULT || "", + pluginDir: process.env.OBSIDIAN_PLUGIN_DIR || "", + build: true, + dryRun: false, + help: false, + }; + + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + + if (arg === "--help" || arg === "-h") { + options.help = true; + } else if (arg === "--no-build") { + options.build = false; + } else if (arg === "--dry-run") { + options.dryRun = true; + } else if (arg === "--vault") { + options.vault = argv[++i] || ""; + } else if (arg === "--plugin-dir") { + options.pluginDir = argv[++i] || ""; + } else { + throw new Error(`Unknown option: ${arg}`); + } + } + + return options; +} + +async function getManifestId() { + const manifestPath = join(root, "manifest.json"); + const manifest = JSON.parse(await readFile(manifestPath, "utf-8")); + + if (!manifest.id || typeof manifest.id !== "string") { + throw new Error("manifest.json is missing a string id"); + } + + return manifest.id; +} + +function runBuild() { + const result = spawnSync("npm", ["run", "build"], { + cwd: root, + stdio: "inherit", + }); + + if (result.error) { + throw result.error; + } + + if (result.status !== 0) { + throw new Error(`npm run build failed with exit code ${result.status}`); + } +} + +async function install() { + const options = parseArgs(process.argv.slice(2)); + + if (options.help) { + printUsage(); + return; + } + + if (options.vault && options.pluginDir) { + throw new Error("Use either --vault or --plugin-dir, not both"); + } + + const manifestId = await getManifestId(); + const targetDir = options.pluginDir + ? resolve(options.pluginDir) + : options.vault + ? join(resolve(options.vault), ".obsidian", "plugins", manifestId) + : ""; + + if (!targetDir) { + printUsage(); + throw new Error("Missing install target. Provide --vault, --plugin-dir, OBSIDIAN_VAULT, or OBSIDIAN_PLUGIN_DIR"); + } + + if (options.build) { + console.log("Building plugin..."); + runBuild(); + } + + console.log(`Installing to ${targetDir}`); + + if (!options.dryRun) { + await mkdir(targetDir, { recursive: true }); + } + + for (const file of runtimeFiles) { + const source = join(root, file); + const destination = join(targetDir, file); + console.log(`${options.dryRun ? "Would copy" : "Copying"} ${file}`); + + if (!options.dryRun) { + await copyFile(source, destination); + } + } + + console.log("Install complete."); +} + +install().catch((error) => { + console.error(error instanceof Error ? error.message : error); + process.exitCode = 1; +});