Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions BlueBlazor.Cli/BlueBlazor.Cli.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Version>1.0.0.1</Version>

<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<PackAsTool>true</PackAsTool>
<ToolCommandName>blueblazor</ToolCommandName>
<PackageOutputPath>./nupkg</PackageOutputPath>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
</ItemGroup>

</Project>
42 changes: 42 additions & 0 deletions BlueBlazor.Cli/Commands/AddCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// File: Commands/AddCommand.cs
using System.CommandLine;
using BlueBlazor.Cli.Services;

namespace BlueBlazor.Cli.Commands;

public static class AddCommand
{
public static Command Create(Option<string?> sourceOption, Option<string?> repoOption)
{
var command = new Command("add", "Adds a component to the target project")
{
sourceOption,
repoOption
};

var nameArg = new Argument<string>("name", "Name of the component");
var targetOption = new Option<DirectoryInfo>(
name: "--target",
description: "Path to the target folder in the Blazor project",
getDefaultValue: () => new DirectoryInfo("./"));

command.AddArgument(nameArg);
command.AddOption(targetOption);

command.SetHandler(async (name, source, repo, target) =>
{
var sourceDir = await SourceResolver.ResolveAsync(source, repo);
if (sourceDir == null)
{
Console.WriteLine($"❌ Source not found: {source}");
return;
}

var installer = new ComponentInstaller(sourceDir, target);
await installer.InstallAsync(name);
},
nameArg, sourceOption, repoOption, targetOption);

return command;
}
}
32 changes: 32 additions & 0 deletions BlueBlazor.Cli/Commands/ListCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// File: Commands/ListCommand.cs
using System.CommandLine;
using BlueBlazor.Cli.Services;

namespace BlueBlazor.Cli.Commands;

public static class ListCommand
{
public static Command Create(Option<string?> sourceOption, Option<string?> repoOption)
{
var command = new Command("list", "Lists available components in the source folder")
{
sourceOption,
repoOption
};

command.SetHandler(async (string? source, string? repo) =>
{
var sourceDir = await SourceResolver.ResolveAsync(source, repo);
if (sourceDir == null)
{
Console.WriteLine($"❌ Source not found: {source}");
return;
}

var lister = new ComponentLister(sourceDir);
lister.ListAvailableComponents();
}, sourceOption, repoOption);

return command;
}
}
13 changes: 13 additions & 0 deletions BlueBlazor.Cli/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.CommandLine;
using BlueBlazor.Cli.Commands;

var sourceOption = new Option<string?>("--source", () => "BlueBlazor/Components", "Path to the local components directory");
var repoOption = new Option<string?>("--repo", () => "https://github.com/bruegmann/blue-blazor.git", "Git repository URL (optional)");

var rootCommand = new RootCommand("BlueBlazor CLI Tool")
{
AddCommand.Create(sourceOption, repoOption),
ListCommand.Create(sourceOption, repoOption)
};

return await rootCommand.InvokeAsync(args);
56 changes: 56 additions & 0 deletions BlueBlazor.Cli/Service/ComponentInstaller.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// 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($"❌ Component \"{baseName}\" was not found.");
return;
}

if (!_targetRoot.Exists)
{
_targetRoot.Create();
}

foreach (var file in matchingFiles)
{
var dest = Path.Combine(_targetRoot.FullName, file.Name);
File.Copy(file.FullName, dest, overwrite: true);
Console.WriteLine($"✅ Copied: {file.Name}");
}

// 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<ComponentMeta>(stream);
Console.WriteLine($"ℹ️ Description: {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; }
}
}
62 changes: 62 additions & 0 deletions BlueBlazor.Cli/Service/ComponentLister.cs
Original file line number Diff line number Diff line change
@@ -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<ComponentMeta>(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; }
}
}
60 changes: 60 additions & 0 deletions BlueBlazor.Cli/Service/SourceResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System.Diagnostics;

namespace BlueBlazor.Cli.Services;

public static class SourceResolver
{
public static async Task<DirectoryInfo?> ResolveAsync(string? source, string? repo)
{
if (!string.IsNullOrWhiteSpace(repo))
{
var repoUri = new Uri(repo);
return await CloneGitRepositoryAsync(repoUri, source);
}

if (!string.IsNullOrWhiteSpace(source))
{
var dir = new DirectoryInfo(source);
return dir.Exists ? dir : null;
}

return null;
}

private static async Task<DirectoryInfo?> CloneGitRepositoryAsync(Uri repoUri, string? relativePath)
{
var repoName = repoUri.Segments.Last().Replace(".git", "");
var tempPath = Path.Combine(Path.GetTempPath(), "blueblazor", repoName);

if (!Directory.Exists(tempPath))
{
Console.WriteLine($"⬇️ Cloning repository: {repoUri}");

var process = Process.Start(new ProcessStartInfo
{
FileName = "git",
Arguments = $"clone --depth=1 {repoUri} \"{tempPath}\"",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false
});

if (process == null)
return null;

await process.WaitForExitAsync();
}
else
{
Console.WriteLine($"🔁 Using cached repository: {tempPath}");
}

// 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;
}
}
6 changes: 6 additions & 0 deletions BlueBlazor.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down