diff --git a/SharpSite.sln b/SharpSite.sln index 7ed7ff9..9782609 100644 --- a/SharpSite.sln +++ b/SharpSite.sln @@ -56,6 +56,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpSite.E2E", "e2e\SharpS EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpSite.Tests.Plugins", "tests\SharpSite.Tests.Plugins\SharpSite.Tests.Plugins.csproj", "{6B629CEE-5AAC-4885-89C6-7BED9DA7CF2C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpSite.PluginPacker", "src\SharpSite.PluginPacker\SharpSite.PluginPacker.csproj", "{677B59E7-C4BA-4024-84D7-78CE6985F3F5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "2. Tools", "2. Tools", "{78F974E0-8074-0543-93D5-DC2AAC8BF3DF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -126,6 +130,10 @@ Global {6B629CEE-5AAC-4885-89C6-7BED9DA7CF2C}.Debug|Any CPU.Build.0 = Debug|Any CPU {6B629CEE-5AAC-4885-89C6-7BED9DA7CF2C}.Release|Any CPU.ActiveCfg = Release|Any CPU {6B629CEE-5AAC-4885-89C6-7BED9DA7CF2C}.Release|Any CPU.Build.0 = Release|Any CPU + {677B59E7-C4BA-4024-84D7-78CE6985F3F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {677B59E7-C4BA-4024-84D7-78CE6985F3F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {677B59E7-C4BA-4024-84D7-78CE6985F3F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {677B59E7-C4BA-4024-84D7-78CE6985F3F5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -147,6 +155,7 @@ Global {BA24379C-40D5-5EDF-63BE-CE5BC727E45D} = {3266CA51-9816-4037-9715-701EB6C2928A} {EFCFB571-6B0C-35CD-6664-160CA5B39244} = {8779454A-1F9C-4705-8EE0-5980C6B9C2A5} {6B629CEE-5AAC-4885-89C6-7BED9DA7CF2C} = {3266CA51-9816-4037-9715-701EB6C2928A} + {677B59E7-C4BA-4024-84D7-78CE6985F3F5} = {78F974E0-8074-0543-93D5-DC2AAC8BF3DF} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {62A15C13-360B-4791-89E9-1FDDFE483970} diff --git a/src/SharpSite.PluginPacker/ArgumentParser.cs b/src/SharpSite.PluginPacker/ArgumentParser.cs new file mode 100644 index 0000000..545f6c3 --- /dev/null +++ b/src/SharpSite.PluginPacker/ArgumentParser.cs @@ -0,0 +1,25 @@ +namespace SharpSite.PluginPacker; + +public static class ArgumentParser +{ + public static (string? inputPath, string? outputPath) ParseArguments(string[] args) + { + string? inputPath = null; + string? outputPath = null; + for (int i = 0; i < args.Length; i++) + { + switch (args[i]) + { + case "-i": + case "--input": + if (i + 1 < args.Length) inputPath = args[++i]; + break; + case "-o": + case "--output": + if (i + 1 < args.Length) outputPath = args[++i]; + break; + } + } + return (inputPath, outputPath); + } +} diff --git a/src/SharpSite.PluginPacker/ManifestHandler.cs b/src/SharpSite.PluginPacker/ManifestHandler.cs new file mode 100644 index 0000000..597dc8a --- /dev/null +++ b/src/SharpSite.PluginPacker/ManifestHandler.cs @@ -0,0 +1,40 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SharpSite.Plugins; + +namespace SharpSite.PluginPacker; + +public static class ManifestHandler +{ + private static readonly JsonSerializerOptions _Opts = new() + { + WriteIndented = true, + Converters = { new JsonStringEnumConverter() } + }; + + public static PluginManifest? LoadOrCreateManifest(string inputPath) + { + string manifestPath = Path.Combine(inputPath, "manifest.json"); + PluginManifest? manifest; + if (!File.Exists(manifestPath)) + { + Console.WriteLine($"manifest.json not found in {inputPath}."); + Console.WriteLine("Let's create one interactively."); + manifest = ManifestPrompter.PromptForManifest(); + var json = JsonSerializer.Serialize(manifest, _Opts); + File.WriteAllText(manifestPath, json); + Console.WriteLine($"Created manifest.json at {manifestPath}"); + } + else + { + var json = File.ReadAllText(manifestPath); + manifest = JsonSerializer.Deserialize(json, _Opts); + if (manifest is null) + { + Console.WriteLine("Failed to parse manifest.json"); + return null; + } + } + return manifest; + } +} diff --git a/src/SharpSite.PluginPacker/ManifestPrompter.cs b/src/SharpSite.PluginPacker/ManifestPrompter.cs new file mode 100644 index 0000000..6ae49b4 --- /dev/null +++ b/src/SharpSite.PluginPacker/ManifestPrompter.cs @@ -0,0 +1,69 @@ +using SharpSite.Plugins; + +namespace SharpSite.PluginPacker; + +public static class ManifestPrompter +{ + private static string PromptRequired(string label) + { + string? value; + do + { + Console.Write($"{label}: "); + value = Console.ReadLine()?.Trim(); + if (string.IsNullOrWhiteSpace(value)) + { + Console.WriteLine($"{label} is required."); + } + } while (string.IsNullOrWhiteSpace(value)); + return value; + } + + public static PluginManifest PromptForManifest() + { + var id = PromptRequired("Id"); + var displayName = PromptRequired("DisplayName"); + var description = PromptRequired("Description"); + var version = PromptRequired("Version"); + var published = PromptRequired("Published (yyyy-MM-dd)"); + var supportedVersions = PromptRequired("SupportedVersions"); + var author = PromptRequired("Author"); + var contact = PromptRequired("Contact"); + var contactEmail = PromptRequired("ContactEmail"); + var authorWebsite = PromptRequired("AuthorWebsite"); + + // Optional fields + Console.Write("Icon (URL): "); + var icon = (Console.ReadLine() ?? "").Trim(); + Console.Write("Source (repository URL): "); + var source = (Console.ReadLine() ?? "").Trim(); + Console.Write("KnownLicense (e.g. MIT, Apache, LGPL): "); + var knownLicense = (Console.ReadLine() ?? "").Trim(); + Console.Write("Tags (comma separated): "); + var tagsStr = (Console.ReadLine() ?? "").Trim(); + var tags = tagsStr.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + Console.Write("Features (comma separated, e.g. Theme,FileStorage): "); + var featuresStr = (Console.ReadLine() ?? "").Trim(); + var features = featuresStr.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var featureEnums = features.Length > 0 ? Array.ConvertAll(features, f => Enum.Parse(f, true)) : []; + return new PluginManifest + { + Id = id, + DisplayName = displayName, + Description = description, + Version = version, + Icon = string.IsNullOrWhiteSpace(icon) ? null : icon, + Published = published, + SupportedVersions = supportedVersions, + Author = author, + Contact = contact, + ContactEmail = contactEmail, + AuthorWebsite = authorWebsite, + Source = string.IsNullOrWhiteSpace(source) ? null : source, + KnownLicense = string.IsNullOrWhiteSpace(knownLicense) ? null : knownLicense, + Tags = tags.Length > 0 ? tags : null, + Features = featureEnums + }; + } +} diff --git a/src/SharpSite.PluginPacker/PluginPackager.cs b/src/SharpSite.PluginPacker/PluginPackager.cs new file mode 100644 index 0000000..b08ad36 --- /dev/null +++ b/src/SharpSite.PluginPacker/PluginPackager.cs @@ -0,0 +1,142 @@ +using System.Diagnostics; +using System.IO.Compression; +using SharpSite.Plugins; + +namespace SharpSite.PluginPacker; + +public static class PluginPackager +{ + public static bool PackagePlugin(string inputPath, string outputPath) + { + // Load manifest + var manifest = ManifestHandler.LoadOrCreateManifest(inputPath); + if (manifest is null) + { + Console.WriteLine("Manifest not found or invalid."); + return false; + } + + // Create temp build output folder + string tempBuildDir = Path.Combine(Path.GetTempPath(), "SharpSitePluginBuild_" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(tempBuildDir); + + // Build the project in Release mode to temp build folder + if (!BuildProject(inputPath, tempBuildDir)) + { + Console.WriteLine("Build failed."); + try { if (Directory.Exists(tempBuildDir)) Directory.Delete(tempBuildDir, true); } catch { } + return false; + } + + // Create temp folder for packaging + string tempDir = Path.Combine(Path.GetTempPath(), "SharpSitePluginPack_" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(tempDir); + try + { + // Copy DLL to lib/ and rename + CopyAndRenameDll(inputPath, tempBuildDir, tempDir, manifest); + + // If Theme, copy .css from wwwroot/ to web/ + if (manifest.Features.Contains(PluginFeatures.Theme)) + { + CopyThemeCssFiles(inputPath, tempDir); + } + // Copy manifest.json and other required files + CopyRequiredFiles(inputPath, tempDir); + // Zip tempDir to outputPath - use proper naming convention ID@VERSION.sspkg + // outputPath is always a directory, generate the filename from manifest + string outFile = Path.Combine(outputPath, $"{manifest.IdVersionToString()}.sspkg"); + + // Ensure the output directory exists + if (!Directory.Exists(outputPath)) + { + Directory.CreateDirectory(outputPath); + } + + if (File.Exists(outFile)) File.Delete(outFile); + ZipFile.CreateFromDirectory(tempDir, outFile); + Console.WriteLine($"Plugin packaged successfully: {outFile}"); + return true; + } + catch (Exception ex) + { + Console.WriteLine($"Packaging failed: {ex.Message}"); + return false; + } + finally + { + // Clean up temp folder + try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { } + try { if (Directory.Exists(tempBuildDir)) Directory.Delete(tempBuildDir, true); } catch { } + } + } + + private static void CopyAndRenameDll(string inputPath, string tempBuildDir, string tempDir, PluginManifest manifest) + { + string libDir = Path.Combine(tempDir, "lib"); + Directory.CreateDirectory(libDir); + string projectName = new DirectoryInfo(inputPath).Name; + string dllSource = Path.Combine(tempBuildDir, projectName + ".dll"); + string dllTarget = Path.Combine(libDir, manifest.Id + ".dll"); + if (!File.Exists(dllSource)) + { + throw new FileNotFoundException($"DLL not found: {dllSource}"); + } + File.Copy(dllSource, dllTarget, overwrite: true); + } + + private static void CopyThemeCssFiles(string inputPath, string tempDir) + { + string webSrc = Path.Combine(inputPath, "wwwroot"); + string webDst = Path.Combine(tempDir, "web"); + if (Directory.Exists(webSrc)) + { + Directory.CreateDirectory(webDst); + foreach (var css in Directory.GetFiles(webSrc, "*.css", SearchOption.AllDirectories)) + { + string dest = Path.Combine(webDst, Path.GetFileName(css)); + File.Copy(css, dest, overwrite: true); + } + } + } + + private static void CopyRequiredFiles(string inputPath, string tempDir) + { + string[] requiredFiles = ["manifest.json", "LICENSE", "README.md", "Changelog.txt"]; + foreach (var file in requiredFiles) + { + string src = Path.Combine(inputPath, file); + if (File.Exists(src)) + { + File.Copy(src, Path.Combine(tempDir, file), overwrite: true); + } + } + } + + private static bool BuildProject(string inputPath, string outputPath) + { + var psi = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = $"build --configuration Release --output \"{outputPath}\"", + WorkingDirectory = inputPath, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + using var proc = Process.Start(psi); + if (proc is null) + { + Console.WriteLine("Failed to start build process."); + return false; + } + proc.WaitForExit(); + if (proc.ExitCode != 0) + { + Console.WriteLine(proc.StandardError.ReadToEnd()); + return false; + } + return true; + } +} diff --git a/src/SharpSite.PluginPacker/Program.cs b/src/SharpSite.PluginPacker/Program.cs new file mode 100644 index 0000000..6e6d78d --- /dev/null +++ b/src/SharpSite.PluginPacker/Program.cs @@ -0,0 +1,45 @@ +using SharpSite.PluginPacker; + +(string? inputPath, string? outputPath) = ArgumentParser.ParseArguments(args); + +if (string.IsNullOrWhiteSpace(inputPath)) +{ + Console.WriteLine("Usage: SharpSite.PluginPacker -i [-o ]"); + Console.WriteLine(" -i, --input Input folder containing the plugin project"); + Console.WriteLine(" -o, --output Output directory (optional, defaults to current directory)"); + Console.WriteLine(); + Console.WriteLine("The output filename will be automatically generated as: ID@VERSION.sspkg"); + return 1; +} + +// Default to current directory if no output path specified +outputPath = string.IsNullOrWhiteSpace(outputPath) ? Directory.GetCurrentDirectory() : outputPath; + +if (!Directory.Exists(inputPath)) +{ + Console.WriteLine($"Input directory '{inputPath}' does not exist."); + return 1; +} + +// Validate that output path is a directory, not a file +if (File.Exists(outputPath)) +{ + Console.WriteLine($"Error: Output path '{outputPath}' points to a file. Please specify a directory."); + return 1; +} + +var manifest = ManifestHandler.LoadOrCreateManifest(inputPath); +if (manifest is null) +{ + Console.WriteLine("Failed to load or create manifest."); + return 1; +} +Console.WriteLine($"Loaded manifest for {manifest.DisplayName} ({manifest.Id})"); + +if (!PluginPackager.PackagePlugin(inputPath, outputPath)) +{ + Console.WriteLine("Packaging failed."); + return 1; +} + +return 0; diff --git a/src/SharpSite.PluginPacker/SharpSite.PluginPacker.csproj b/src/SharpSite.PluginPacker/SharpSite.PluginPacker.csproj new file mode 100644 index 0000000..6d9085a --- /dev/null +++ b/src/SharpSite.PluginPacker/SharpSite.PluginPacker.csproj @@ -0,0 +1,14 @@ + + + + + + + + Exe + net9.0 + enable + enable + + +