From 964bf7618ebd7f5e72108c8423cf9c290ef3ef6b Mon Sep 17 00:00:00 2001 From: lgk-bsw Date: Tue, 8 Apr 2025 14:09:41 +0200 Subject: [PATCH 1/2] BLUE-18 With help of ChatGPT created CLI --- BlueBlazor.Cli/BlueBlazor.Cli.csproj | 18 +++++ BlueBlazor.Cli/Commands/AddCommand.cs | 43 +++++++++++ BlueBlazor.Cli/Commands/ListCommand.cs | 35 +++++++++ BlueBlazor.Cli/Program.cs | 10 +++ BlueBlazor.Cli/Service/ComponentInstaller.cs | 54 ++++++++++++++ BlueBlazor.Cli/Service/ComponentLister.cs | 62 ++++++++++++++++ BlueBlazor.Cli/Service/SourceResolver.cs | 75 ++++++++++++++++++++ BlueBlazor.sln | 6 ++ 8 files changed, 303 insertions(+) create mode 100644 BlueBlazor.Cli/BlueBlazor.Cli.csproj create mode 100644 BlueBlazor.Cli/Commands/AddCommand.cs create mode 100644 BlueBlazor.Cli/Commands/ListCommand.cs create mode 100644 BlueBlazor.Cli/Program.cs create mode 100644 BlueBlazor.Cli/Service/ComponentInstaller.cs create mode 100644 BlueBlazor.Cli/Service/ComponentLister.cs create mode 100644 BlueBlazor.Cli/Service/SourceResolver.cs diff --git a/BlueBlazor.Cli/BlueBlazor.Cli.csproj b/BlueBlazor.Cli/BlueBlazor.Cli.csproj new file mode 100644 index 0000000..e992656 --- /dev/null +++ b/BlueBlazor.Cli/BlueBlazor.Cli.csproj @@ -0,0 +1,18 @@ + + + + Exe + net8.0 + enable + enable + + true + blueblazor + ./nupkg + + + + + + + diff --git a/BlueBlazor.Cli/Commands/AddCommand.cs b/BlueBlazor.Cli/Commands/AddCommand.cs new file mode 100644 index 0000000..e9d8f2b --- /dev/null +++ b/BlueBlazor.Cli/Commands/AddCommand.cs @@ -0,0 +1,43 @@ +// File: Commands/AddCommand.cs +using System.CommandLine; +using BlueBlazor.Cli.Services; + +namespace BlueBlazor.Cli.Commands; + +public static class AddCommand +{ + public static Command Create() + { + var command = new Command("add", "Adds a component to the target project"); + + var nameArg = new Argument("name", "Name of the component"); + var sourceOption = new Option( + name: "--source", + description: "Path to the component source (local or GitHub)", + getDefaultValue: () => "./Components"); + var targetOption = new Option( + name: "--target", + description: "Path to the target folder in the Blazor project", + getDefaultValue: () => new DirectoryInfo("./")); + + command.AddArgument(nameArg); + command.AddOption(sourceOption); + command.AddOption(targetOption); + + command.SetHandler(async (name, source, target) => + { + var sourceDir = await SourceResolver.ResolveAsync(source); + if (sourceDir == null) + { + Console.WriteLine($"❌ Source not found: {source}"); + return; + } + + var installer = new ComponentInstaller(sourceDir, target); + await installer.InstallAsync(name); + }, + nameArg, sourceOption, targetOption); + + return command; + } +} diff --git a/BlueBlazor.Cli/Commands/ListCommand.cs b/BlueBlazor.Cli/Commands/ListCommand.cs new file mode 100644 index 0000000..0fc01ec --- /dev/null +++ b/BlueBlazor.Cli/Commands/ListCommand.cs @@ -0,0 +1,35 @@ +// File: Commands/ListCommand.cs +using System.CommandLine; +using BlueBlazor.Cli.Services; + +namespace BlueBlazor.Cli.Commands; + +public static class ListCommand +{ + public static Command Create() + { + var command = new Command("list", "Lists available components in the source folder"); + + var sourceOption = new Option( + name: "--source", + description: "Path to the component source (local or GitHub)", + getDefaultValue: () => "./Components"); + + command.AddOption(sourceOption); + + command.SetHandler(async source => + { + var sourceDir = await SourceResolver.ResolveAsync(source); + if (sourceDir == null) + { + Console.WriteLine($"❌ Source not found: {source}"); + return; + } + + var lister = new ComponentLister(sourceDir); + lister.ListAvailableComponents(); + }, sourceOption); + + return command; + } +} diff --git a/BlueBlazor.Cli/Program.cs b/BlueBlazor.Cli/Program.cs new file mode 100644 index 0000000..7402387 --- /dev/null +++ b/BlueBlazor.Cli/Program.cs @@ -0,0 +1,10 @@ +using System.CommandLine; +using BlueBlazor.Cli.Commands; + +var rootCommand = new RootCommand("BlueBlazor CLI Tool") +{ + AddCommand.Create(), + ListCommand.Create() +}; + +return await rootCommand.InvokeAsync(args); diff --git a/BlueBlazor.Cli/Service/ComponentInstaller.cs b/BlueBlazor.Cli/Service/ComponentInstaller.cs new file mode 100644 index 0000000..c62b58c --- /dev/null +++ b/BlueBlazor.Cli/Service/ComponentInstaller.cs @@ -0,0 +1,54 @@ +// File: Services/ComponentInstaller.cs +using System.Text.Json; + +namespace BlueBlazor.Cli.Services; + +public class ComponentInstaller +{ + private readonly DirectoryInfo _sourceRoot; + private readonly DirectoryInfo _targetRoot; + + public ComponentInstaller(DirectoryInfo sourceRoot, DirectoryInfo targetRoot) + { + _sourceRoot = sourceRoot; + _targetRoot = targetRoot; + } + + public async Task InstallAsync(string name) + { + var baseName = Path.GetFileNameWithoutExtension(name); + var matchingFiles = _sourceRoot.GetFiles($"{baseName}.*", SearchOption.TopDirectoryOnly); + if (matchingFiles.Length == 0) + { + Console.WriteLine($"❌ Komponente \"{baseName}\" wurde nicht gefunden."); + return; + } + + var targetDir = Path.Combine(_targetRoot.FullName, baseName); + Directory.CreateDirectory(targetDir); + + foreach (var file in matchingFiles) + { + var dest = Path.Combine(targetDir, file.Name); + File.Copy(file.FullName, dest, overwrite: true); + Console.WriteLine($"✅ Kopiert: {file.Name}"); + } + + // Optional: Metadaten ausgeben + var metaFile = matchingFiles.FirstOrDefault(f => f.Name == $"{baseName}.meta.json"); + if (metaFile != null) + { + using var stream = metaFile.OpenRead(); + var meta = await JsonSerializer.DeserializeAsync(stream); + Console.WriteLine($"ℹ️ Beschreibung: {meta?.Description}"); + } + } + + private class ComponentMeta + { + public string? Name { get; set; } + public string? Version { get; set; } + public string? Description { get; set; } + public string[]? Files { get; set; } + } +} diff --git a/BlueBlazor.Cli/Service/ComponentLister.cs b/BlueBlazor.Cli/Service/ComponentLister.cs new file mode 100644 index 0000000..6ac5c5d --- /dev/null +++ b/BlueBlazor.Cli/Service/ComponentLister.cs @@ -0,0 +1,62 @@ +// File: Services/ComponentLister.cs +using System.Text.Json; + +namespace BlueBlazor.Cli.Services; + +public class ComponentLister +{ + private readonly DirectoryInfo _sourceRoot; + + public ComponentLister(DirectoryInfo sourceRoot) + { + _sourceRoot = sourceRoot; + } + + public void ListAvailableComponents() + { + if (!_sourceRoot.Exists) + { + Console.WriteLine($"❌ Source folder not found: {_sourceRoot.FullName}"); + return; + } + + var metaFiles = _sourceRoot.GetFiles("*.meta.json", SearchOption.TopDirectoryOnly); + + if (metaFiles.Length == 0) + { + Console.WriteLine("ℹ️ No metadata files found. Listing by convention:"); + + var components = _sourceRoot.GetFiles("*.razor") + .Select(f => Path.GetFileNameWithoutExtension(f.Name)) + .Distinct() + .OrderBy(n => n); + + foreach (var comp in components) + { + Console.WriteLine($"• {comp}"); + } + + return; + } + + foreach (var file in metaFiles) + { + try + { + using var stream = file.OpenRead(); + var meta = JsonSerializer.Deserialize(stream); + Console.WriteLine($"• {meta?.Name ?? file.Name.Replace(".meta.json", "")} - {meta?.Description}"); + } + catch + { + Console.WriteLine($"• {file.Name.Replace(".meta.json", "")} (invalid metadata)"); + } + } + } + + private class ComponentMeta + { + public string? Name { get; set; } + public string? Description { get; set; } + } +} diff --git a/BlueBlazor.Cli/Service/SourceResolver.cs b/BlueBlazor.Cli/Service/SourceResolver.cs new file mode 100644 index 0000000..e71ee37 --- /dev/null +++ b/BlueBlazor.Cli/Service/SourceResolver.cs @@ -0,0 +1,75 @@ +namespace BlueBlazor.Cli.Services; + +public static class SourceResolver +{ + public static async Task ResolveAsync(string source) + { + if (Uri.TryCreate(source, UriKind.Absolute, out var uri) && + uri.Host.Contains("github.com")) + { + return await CloneGitHubRepoAsync(uri); + } + + var dir = new DirectoryInfo(source); + return dir.Exists ? dir : null; + } + + private static async Task CloneGitHubRepoAsync(Uri uri) + { + // Prüfen, ob die URL auf einen spezifischen Branch oder ein Verzeichnis verweist + var repoUrl = uri.ToString(); + var repoPath = repoUrl.Split("tree/").FirstOrDefault(); // Nur den Git-Repo-Link extrahieren + var directoryPath = uri.ToString().Split("tree/").LastOrDefault(); // Der Pfad nach "tree/" + + if (string.IsNullOrEmpty(repoPath) || string.IsNullOrEmpty(directoryPath)) + { + Console.WriteLine($"❌ Invalid GitHub URL: {uri}"); + return null; + } + + // Extrahiere den Branchnamen aus der URL, falls vorhanden + var branchName = repoUrl.Split("tree/").LastOrDefault()?.Split('/').FirstOrDefault(); + + if (string.IsNullOrEmpty(branchName)) + { + Console.WriteLine($"❌ No branch found in the URL: {uri}"); + return null; + } + + // Klone das Repository mit dem Branchnamen + var repoName = repoPath.TrimEnd('/'); + var tempPath = Path.Combine(Path.GetTempPath(), "blueblazor", repoName.Split('/').Last()); + + if (Directory.Exists(tempPath)) + { + Console.WriteLine($"🔁 Using cached repository: {tempPath}"); + } + else + { + Console.WriteLine($"⬇️ Cloning repository: {repoName} (Branch: {branchName})"); + var process = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo + { + FileName = "git", + Arguments = $"clone --depth=1 --branch {branchName} {repoName}.git \"{tempPath}\"", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false + }); + + if (process == null) return null; + + await process.WaitForExitAsync(); + } + + // Navigiere zum richtigen Verzeichnis im geklonten Repository + var sourceDir = new DirectoryInfo(Path.Combine(tempPath, directoryPath)); + + if (sourceDir.Exists) + { + return sourceDir; + } + + Console.WriteLine($"❌ Directory not found: {directoryPath}"); + return null; + } +} diff --git a/BlueBlazor.sln b/BlueBlazor.sln index df5008a..109272d 100644 --- a/BlueBlazor.sln +++ b/BlueBlazor.sln @@ -22,6 +22,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorWebAppAutoPerPage.Cli EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlueBlazor.Tests", "BlueBlazor.Tests\BlueBlazor.Tests.csproj", "{FEE262AB-0615-4368-8F23-F31FC255E15C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlueBlazor.Cli", "BlueBlazor.Cli\BlueBlazor.Cli.csproj", "{428409D9-601F-47DD-830F-34850DFD2EB6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -56,6 +58,10 @@ Global {FEE262AB-0615-4368-8F23-F31FC255E15C}.Debug|Any CPU.Build.0 = Debug|Any CPU {FEE262AB-0615-4368-8F23-F31FC255E15C}.Release|Any CPU.ActiveCfg = Release|Any CPU {FEE262AB-0615-4368-8F23-F31FC255E15C}.Release|Any CPU.Build.0 = Release|Any CPU + {428409D9-601F-47DD-830F-34850DFD2EB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {428409D9-601F-47DD-830F-34850DFD2EB6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {428409D9-601F-47DD-830F-34850DFD2EB6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {428409D9-601F-47DD-830F-34850DFD2EB6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 72eed911acda09054616e95ea82b714ac9743c74 Mon Sep 17 00:00:00 2001 From: lgk-bsw Date: Mon, 14 Apr 2025 09:37:20 +0200 Subject: [PATCH 2/2] BLUE-18 Repo param --- BlueBlazor.Cli/BlueBlazor.Cli.csproj | 2 + BlueBlazor.Cli/Commands/AddCommand.cs | 19 +++-- BlueBlazor.Cli/Commands/ListCommand.cs | 21 +++--- BlueBlazor.Cli/Program.cs | 7 +- BlueBlazor.Cli/Service/ComponentInstaller.cs | 16 ++-- BlueBlazor.Cli/Service/SourceResolver.cs | 79 ++++++++------------ 6 files changed, 66 insertions(+), 78 deletions(-) diff --git a/BlueBlazor.Cli/BlueBlazor.Cli.csproj b/BlueBlazor.Cli/BlueBlazor.Cli.csproj index e992656..a37bf7d 100644 --- a/BlueBlazor.Cli/BlueBlazor.Cli.csproj +++ b/BlueBlazor.Cli/BlueBlazor.Cli.csproj @@ -1,6 +1,8 @@  + 1.0.0.1 + Exe net8.0 enable diff --git a/BlueBlazor.Cli/Commands/AddCommand.cs b/BlueBlazor.Cli/Commands/AddCommand.cs index e9d8f2b..5d01093 100644 --- a/BlueBlazor.Cli/Commands/AddCommand.cs +++ b/BlueBlazor.Cli/Commands/AddCommand.cs @@ -6,27 +6,26 @@ namespace BlueBlazor.Cli.Commands; public static class AddCommand { - public static Command Create() + public static Command Create(Option sourceOption, Option repoOption) { - var command = new Command("add", "Adds a component to the target project"); + var command = new Command("add", "Adds a component to the target project") + { + sourceOption, + repoOption + }; var nameArg = new Argument("name", "Name of the component"); - var sourceOption = new Option( - name: "--source", - description: "Path to the component source (local or GitHub)", - getDefaultValue: () => "./Components"); var targetOption = new Option( name: "--target", description: "Path to the target folder in the Blazor project", getDefaultValue: () => new DirectoryInfo("./")); command.AddArgument(nameArg); - command.AddOption(sourceOption); command.AddOption(targetOption); - command.SetHandler(async (name, source, target) => + command.SetHandler(async (name, source, repo, target) => { - var sourceDir = await SourceResolver.ResolveAsync(source); + var sourceDir = await SourceResolver.ResolveAsync(source, repo); if (sourceDir == null) { Console.WriteLine($"❌ Source not found: {source}"); @@ -36,7 +35,7 @@ public static Command Create() var installer = new ComponentInstaller(sourceDir, target); await installer.InstallAsync(name); }, - nameArg, sourceOption, targetOption); + nameArg, sourceOption, repoOption, targetOption); return command; } diff --git a/BlueBlazor.Cli/Commands/ListCommand.cs b/BlueBlazor.Cli/Commands/ListCommand.cs index 0fc01ec..0fef328 100644 --- a/BlueBlazor.Cli/Commands/ListCommand.cs +++ b/BlueBlazor.Cli/Commands/ListCommand.cs @@ -6,20 +6,17 @@ namespace BlueBlazor.Cli.Commands; public static class ListCommand { - public static Command Create() + public static Command Create(Option sourceOption, Option repoOption) { - var command = new Command("list", "Lists available components in the source folder"); - - var sourceOption = new Option( - name: "--source", - description: "Path to the component source (local or GitHub)", - getDefaultValue: () => "./Components"); - - command.AddOption(sourceOption); + var command = new Command("list", "Lists available components in the source folder") + { + sourceOption, + repoOption + }; - command.SetHandler(async source => + command.SetHandler(async (string? source, string? repo) => { - var sourceDir = await SourceResolver.ResolveAsync(source); + var sourceDir = await SourceResolver.ResolveAsync(source, repo); if (sourceDir == null) { Console.WriteLine($"❌ Source not found: {source}"); @@ -28,7 +25,7 @@ public static Command Create() var lister = new ComponentLister(sourceDir); lister.ListAvailableComponents(); - }, sourceOption); + }, sourceOption, repoOption); return command; } diff --git a/BlueBlazor.Cli/Program.cs b/BlueBlazor.Cli/Program.cs index 7402387..8a5f77a 100644 --- a/BlueBlazor.Cli/Program.cs +++ b/BlueBlazor.Cli/Program.cs @@ -1,10 +1,13 @@ using System.CommandLine; using BlueBlazor.Cli.Commands; +var sourceOption = new Option("--source", () => "BlueBlazor/Components", "Path to the local components directory"); +var repoOption = new Option("--repo", () => "https://github.com/bruegmann/blue-blazor.git", "Git repository URL (optional)"); + var rootCommand = new RootCommand("BlueBlazor CLI Tool") { - AddCommand.Create(), - ListCommand.Create() + AddCommand.Create(sourceOption, repoOption), + ListCommand.Create(sourceOption, repoOption) }; return await rootCommand.InvokeAsync(args); diff --git a/BlueBlazor.Cli/Service/ComponentInstaller.cs b/BlueBlazor.Cli/Service/ComponentInstaller.cs index c62b58c..ac33377 100644 --- a/BlueBlazor.Cli/Service/ComponentInstaller.cs +++ b/BlueBlazor.Cli/Service/ComponentInstaller.cs @@ -20,27 +20,29 @@ public async Task InstallAsync(string name) var matchingFiles = _sourceRoot.GetFiles($"{baseName}.*", SearchOption.TopDirectoryOnly); if (matchingFiles.Length == 0) { - Console.WriteLine($"❌ Komponente \"{baseName}\" wurde nicht gefunden."); + Console.WriteLine($"❌ Component \"{baseName}\" was not found."); return; } - var targetDir = Path.Combine(_targetRoot.FullName, baseName); - Directory.CreateDirectory(targetDir); + if (!_targetRoot.Exists) + { + _targetRoot.Create(); + } foreach (var file in matchingFiles) { - var dest = Path.Combine(targetDir, file.Name); + var dest = Path.Combine(_targetRoot.FullName, file.Name); File.Copy(file.FullName, dest, overwrite: true); - Console.WriteLine($"✅ Kopiert: {file.Name}"); + Console.WriteLine($"✅ Copied: {file.Name}"); } - // Optional: Metadaten ausgeben + // Optional: Display metadata var metaFile = matchingFiles.FirstOrDefault(f => f.Name == $"{baseName}.meta.json"); if (metaFile != null) { using var stream = metaFile.OpenRead(); var meta = await JsonSerializer.DeserializeAsync(stream); - Console.WriteLine($"ℹ️ Beschreibung: {meta?.Description}"); + Console.WriteLine($"ℹ️ Description: {meta?.Description}"); } } diff --git a/BlueBlazor.Cli/Service/SourceResolver.cs b/BlueBlazor.Cli/Service/SourceResolver.cs index e71ee37..eab20c8 100644 --- a/BlueBlazor.Cli/Service/SourceResolver.cs +++ b/BlueBlazor.Cli/Service/SourceResolver.cs @@ -1,75 +1,60 @@ -namespace BlueBlazor.Cli.Services; +using System.Diagnostics; + +namespace BlueBlazor.Cli.Services; public static class SourceResolver { - public static async Task ResolveAsync(string source) + public static async Task ResolveAsync(string? source, string? repo) { - if (Uri.TryCreate(source, UriKind.Absolute, out var uri) && - uri.Host.Contains("github.com")) + if (!string.IsNullOrWhiteSpace(repo)) { - return await CloneGitHubRepoAsync(uri); + var repoUri = new Uri(repo); + return await CloneGitRepositoryAsync(repoUri, source); } - var dir = new DirectoryInfo(source); - return dir.Exists ? dir : null; - } - - private static async Task CloneGitHubRepoAsync(Uri uri) - { - // Prüfen, ob die URL auf einen spezifischen Branch oder ein Verzeichnis verweist - var repoUrl = uri.ToString(); - var repoPath = repoUrl.Split("tree/").FirstOrDefault(); // Nur den Git-Repo-Link extrahieren - var directoryPath = uri.ToString().Split("tree/").LastOrDefault(); // Der Pfad nach "tree/" - - if (string.IsNullOrEmpty(repoPath) || string.IsNullOrEmpty(directoryPath)) + if (!string.IsNullOrWhiteSpace(source)) { - Console.WriteLine($"❌ Invalid GitHub URL: {uri}"); - return null; + var dir = new DirectoryInfo(source); + return dir.Exists ? dir : null; } - // Extrahiere den Branchnamen aus der URL, falls vorhanden - var branchName = repoUrl.Split("tree/").LastOrDefault()?.Split('/').FirstOrDefault(); - - if (string.IsNullOrEmpty(branchName)) - { - Console.WriteLine($"❌ No branch found in the URL: {uri}"); - return null; - } + return null; + } - // Klone das Repository mit dem Branchnamen - var repoName = repoPath.TrimEnd('/'); - var tempPath = Path.Combine(Path.GetTempPath(), "blueblazor", repoName.Split('/').Last()); + private static async Task CloneGitRepositoryAsync(Uri repoUri, string? relativePath) + { + var repoName = repoUri.Segments.Last().Replace(".git", ""); + var tempPath = Path.Combine(Path.GetTempPath(), "blueblazor", repoName); - if (Directory.Exists(tempPath)) + if (!Directory.Exists(tempPath)) { - Console.WriteLine($"🔁 Using cached repository: {tempPath}"); - } - else - { - Console.WriteLine($"⬇️ Cloning repository: {repoName} (Branch: {branchName})"); - var process = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo + Console.WriteLine($"⬇️ Cloning repository: {repoUri}"); + + var process = Process.Start(new ProcessStartInfo { FileName = "git", - Arguments = $"clone --depth=1 --branch {branchName} {repoName}.git \"{tempPath}\"", + Arguments = $"clone --depth=1 {repoUri} \"{tempPath}\"", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false }); - if (process == null) return null; + if (process == null) + return null; await process.WaitForExitAsync(); } - - // Navigiere zum richtigen Verzeichnis im geklonten Repository - var sourceDir = new DirectoryInfo(Path.Combine(tempPath, directoryPath)); - - if (sourceDir.Exists) + else { - return sourceDir; + Console.WriteLine($"🔁 Using cached repository: {tempPath}"); } - Console.WriteLine($"❌ Directory not found: {directoryPath}"); - return null; + // Navigate to subfolder if --source was set + var finalPath = !string.IsNullOrWhiteSpace(relativePath) + ? Path.Combine(tempPath, relativePath) + : tempPath; + + var finalDir = new DirectoryInfo(finalPath); + return finalDir.Exists ? finalDir : null; } }