Build tool
This commit is contained in:
@@ -0,0 +1,304 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user