using System.Diagnostics; using System.Text.Json; using System.Text.RegularExpressions; const string DocsFolderName = "op.docs"; const string PluginSourceFolder = "OP_plugin"; const string PluginManifestFileName = "manifest.json"; try { var buildProjectDir = GetBuildProjectDirectory(); var workspaceRoot = Path.GetFullPath(Path.Combine(buildProjectDir, "..")); var docsRoot = Path.GetFullPath(Path.Combine(workspaceRoot, "..", DocsFolderName)); var pluginSourceRoot = Path.GetFullPath(Path.Combine(workspaceRoot, "..", PluginSourceFolder)); Console.WriteLine($"Workspace root: {workspaceRoot}"); Console.WriteLine($"Docs root: {docsRoot}"); Console.WriteLine($"Plugin source: {pluginSourceRoot}"); EnsureDirectoryExists(docsRoot, "Docs folder not found. Make sure op.docs exists in the workspace root."); EnsureDirectoryExists(pluginSourceRoot, "Plugin source folder not found. Make sure OP_plugin exists in the workspace root."); var manifestPath = Path.Combine(pluginSourceRoot, PluginManifestFileName); if (!File.Exists(manifestPath)) throw new InvalidOperationException($"Plugin manifest not found at {manifestPath}"); BuildPlugin(pluginSourceRoot); var pluginId = ReadPluginId(manifestPath); Console.WriteLine($"Detected plugin id: {pluginId}"); var userRoot = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); var docsTargets = ScanDocTargets(docsRoot); if (!docsTargets.Any()) { Console.WriteLine("No docs files with 'category: docs' were found."); return; } var uniqueTargets = docsTargets.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); Console.WriteLine($"Found {uniqueTargets.Count} unique docs target(s):"); foreach (var target in uniqueTargets) { Console.WriteLine($" - {target}"); } foreach (var targetPath in uniqueTargets) { var targetRoot = BuildAbsolutePathFromUserRoot(userRoot, targetPath); var pluginsRoot = Path.Combine(targetRoot, ".obsidian", "plugins"); var pluginDestination = Path.Combine(pluginsRoot, pluginId); Console.WriteLine(); Console.WriteLine($"Installing plugin to: {pluginDestination}"); if (Directory.Exists(pluginDestination)) { Console.WriteLine("Existing plugin folder found. Deleting previous version..."); Directory.Delete(pluginDestination, recursive: true); } Directory.CreateDirectory(pluginsRoot); CopyDirectory(pluginSourceRoot, pluginDestination, GetCopyExcludes()); Console.WriteLine("Plugin copied successfully."); } } catch (Exception ex) { Console.Error.WriteLine("ERROR: " + ex.Message); Environment.Exit(1); } static string GetBuildProjectDirectory() { var baseDir = AppContext.BaseDirectory; // Current baseDir is Build/bin/Debug/net10.0/ // We want to go up to the project root (where Build/ and OP.sln are) return Path.GetFullPath(Path.Combine(baseDir, "..", "..", "..")); } static void EnsureDirectoryExists(string path, string message) { if (!Directory.Exists(path)) throw new DirectoryNotFoundException(message); } static IReadOnlyCollection ScanDocTargets(string docsRoot) { var docsTargetPaths = new List(); var markdownFiles = Directory.EnumerateFiles(docsRoot, "*.md", SearchOption.AllDirectories); foreach (var filePath in markdownFiles) { var content = File.ReadAllText(filePath); var frontMatter = ExtractFrontMatter(content); if (frontMatter is null) continue; if (!frontMatter.TryGetValue("category", out var categoryValue) || !string.Equals(categoryValue.Trim(), "docs", StringComparison.OrdinalIgnoreCase)) { continue; } if (!frontMatter.TryGetValue("path", out var pathValue) || string.IsNullOrWhiteSpace(pathValue)) { Console.WriteLine($"WARNING: File '{filePath}' has category: docs but missing path."); continue; } docsTargetPaths.Add(pathValue.Trim()); } return docsTargetPaths; } static Dictionary? ExtractFrontMatter(string content) { var match = Regex.Match(content, @"\A---\r?\n(.*?)\r?\n---", RegexOptions.Singleline); if (!match.Success) return null; var frontMatter = new Dictionary(StringComparer.OrdinalIgnoreCase); var body = match.Groups[1].Value; foreach (var rawLine in body.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)) { var line = rawLine.Trim(); if (line.Length == 0 || line.StartsWith("#")) continue; var separatorIndex = line.IndexOf(':'); if (separatorIndex < 0) continue; var key = line.Substring(0, separatorIndex).Trim(); var value = line.Substring(separatorIndex + 1).Trim().Trim('"').Trim(); frontMatter[key] = value; } return frontMatter; } static string BuildAbsolutePathFromUserRoot(string userRoot, string relativePath) { if (Path.IsPathRooted(relativePath)) { return Path.GetFullPath(relativePath); } var segments = relativePath .Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries) .Select(segment => segment.Trim()) .Where(segment => segment.Length > 0) .ToArray(); if (segments.Length == 0) throw new InvalidOperationException($"Invalid path value: '{relativePath}'"); return Path.GetFullPath(Path.Combine(new[] { userRoot }.Concat(segments).ToArray())); } static string ReadPluginId(string manifestPath) { using var stream = File.OpenRead(manifestPath); using var doc = JsonDocument.Parse(stream); if (!doc.RootElement.TryGetProperty("id", out var idElement)) throw new InvalidOperationException("Plugin manifest does not contain an 'id' property."); var pluginId = idElement.GetString(); if (string.IsNullOrWhiteSpace(pluginId)) throw new InvalidOperationException("Plugin id is empty."); return pluginId.Trim(); } static void BuildPlugin(string pluginSourceRoot) { Console.WriteLine("Building plugin in OP_plugin..."); var npmExecutable = FindNpmExecutable(); if (npmExecutable is null) { throw new InvalidOperationException("Could not find npm on PATH. Install Node.js/npm or add npm to your PATH."); } var processStartInfo = new ProcessStartInfo(npmExecutable) { WorkingDirectory = pluginSourceRoot, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, }; processStartInfo.FileName = "cmd"; processStartInfo.ArgumentList.Add("/c"); processStartInfo.ArgumentList.Add(npmExecutable); processStartInfo.ArgumentList.Add("run"); processStartInfo.ArgumentList.Add("build"); using var process = Process.Start(processStartInfo) ?? throw new InvalidOperationException($"Failed to start 'cmd /c {npmExecutable}'."); var stdout = process.StandardOutput.ReadToEnd(); var stderr = process.StandardError.ReadToEnd(); process.WaitForExit(); Console.WriteLine(stdout); if (!string.IsNullOrWhiteSpace(stderr)) Console.Error.WriteLine(stderr); if (process.ExitCode != 0) throw new InvalidOperationException($"Plugin build failed with exit code {process.ExitCode}."); Console.WriteLine("Plugin build completed successfully."); } static string? FindNpmExecutable() { var candidates = new[] { "npm.cmd", "npm.exe", "npm" }; foreach (var candidate in candidates) { try { var processStartInfo = new ProcessStartInfo("cmd", $"/c {candidate} --version") { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, }; using var process = Process.Start(processStartInfo); if (process is null) continue; bool exited = process.WaitForExit(5000); if (!exited) { process.Kill(); continue; } if (process.ExitCode == 0) return candidate; } catch { continue; } } return null; } static HashSet GetCopyExcludes() { return new(StringComparer.OrdinalIgnoreCase) { "node_modules", "src", "obj", "bin", ".git", ".idea", ".vscode", ".editorconfig", ".npmrc", "package.json", "package-lock.json", "tsconfig.json", "esbuild.config.mjs", "eslint.config.mts", "version-bump.mjs", "versions.json", "data.json", "README.md", "AGENTS.md", "LICENSE" }; } static void CopyDirectory(string sourceDir, string destinationDir, HashSet excludes) { Directory.CreateDirectory(destinationDir); foreach (var filePath in Directory.GetFiles(sourceDir)) { var fileName = Path.GetFileName(filePath); if (excludes.Contains(fileName)) continue; var destinationFile = Path.Combine(destinationDir, fileName); File.Copy(filePath, destinationFile, overwrite: true); } foreach (var directoryPath in Directory.GetDirectories(sourceDir)) { var dirName = Path.GetFileName(directoryPath); if (excludes.Contains(dirName)) continue; CopyDirectory(directoryPath, Path.Combine(destinationDir, dirName), excludes); } }