Files
Obsidian-Plugins/OP/Build/Program.cs
T
2026-06-19 16:52:38 -04:00

305 lines
9.6 KiB
C#

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<string> ScanDocTargets(string docsRoot)
{
var docsTargetPaths = new List<string>();
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<string, string>? ExtractFrontMatter(string content)
{
var match = Regex.Match(content, @"\A---\r?\n(.*?)\r?\n---", RegexOptions.Singleline);
if (!match.Success)
return null;
var frontMatter = new Dictionary<string, string>(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<string> 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<string> 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);
}
}