305 lines
9.6 KiB
C#
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);
|
|
}
|
|
}
|