feat: Dynamic command loader, default command with module registry
This commit is contained in:
parent
a7cae86254
commit
cea3d11a41
1
.gitignore
vendored
1
.gitignore
vendored
@ -402,3 +402,4 @@ FodyWeavers.xsd
|
|||||||
# End of https://www.toptal.com/developers/gitignore/api/csharp
|
# End of https://www.toptal.com/developers/gitignore/api/csharp
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.vscode
|
.vscode
|
||||||
|
.idea
|
||||||
|
@ -8,7 +8,11 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Spectre.Console.Cli" Version="0.50.1-preview.0.5" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0-preview.3.25171.5" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0-preview.3.25171.5" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0-preview.3.25171.5" />
|
||||||
|
<PackageReference Include="Spectre.Console" Version="0.50.0" />
|
||||||
|
<PackageReference Include="Spectre.Console.Cli" Version="0.50.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
26
Commands/Container/Module.cs
Normal file
26
Commands/Container/Module.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using Automancer.Command.Image;
|
||||||
|
using Automancer.Common;
|
||||||
|
using Spectre.Console.Cli;
|
||||||
|
|
||||||
|
namespace Automancer.Command.Container;
|
||||||
|
|
||||||
|
public class Module : ICommandModuleWithRegistry
|
||||||
|
{
|
||||||
|
public void Configure(IConfigurator config)
|
||||||
|
{
|
||||||
|
// No implementation needed here
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Configure(IConfigurator config, CommandRegistry registry)
|
||||||
|
{
|
||||||
|
config.AddBranch("container", container =>
|
||||||
|
{
|
||||||
|
var description = "Container operations";
|
||||||
|
|
||||||
|
container.SetDescription(description);
|
||||||
|
registry.Add("container", description);
|
||||||
|
|
||||||
|
container.AddCommand<PsCommand>("ps").WithDescription("List containers");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
31
Commands/Container/PsCommand.cs
Normal file
31
Commands/Container/PsCommand.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using Spectre.Console.Cli;
|
||||||
|
|
||||||
|
namespace Automancer.Command.Image;
|
||||||
|
|
||||||
|
public class PsSettings: CommandSettings
|
||||||
|
{
|
||||||
|
[CommandArgument(0, "[path]")]
|
||||||
|
[Description("Path to the Dockerfile")]
|
||||||
|
public string? Path { get; set; }
|
||||||
|
|
||||||
|
[CommandOption("-t|--tag")]
|
||||||
|
[Description("Tag to use for the image")]
|
||||||
|
public string? Tag { get; set; }
|
||||||
|
|
||||||
|
[CommandOption("-f|--file")]
|
||||||
|
[Description("Path to the Dockerfile")]
|
||||||
|
public string? File { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PsCommand: Command<BuldSettings>
|
||||||
|
{
|
||||||
|
public override int Execute(CommandContext context, BuldSettings settings)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Building image from {settings.Path} with tag {settings.Tag}");
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
34
Commands/Image/BuildCommand.cs
Normal file
34
Commands/Image/BuildCommand.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using Spectre.Console.Cli;
|
||||||
|
|
||||||
|
namespace Automancer.Command.Image;
|
||||||
|
|
||||||
|
public class BuldSettings: CommandSettings
|
||||||
|
{
|
||||||
|
[CommandArgument(0, "[path]")]
|
||||||
|
[Description("Path to the Dockerfile")]
|
||||||
|
public string? Path { get; set; }
|
||||||
|
|
||||||
|
[CommandOption("-t|--tag")]
|
||||||
|
[Description("Tag to use for the image")]
|
||||||
|
public string? Tag { get; set; }
|
||||||
|
|
||||||
|
[CommandOption("-f|--file")]
|
||||||
|
[Description("Path to the Dockerfile")]
|
||||||
|
public string? File { get; set; }
|
||||||
|
|
||||||
|
[CommandOption("--push")]
|
||||||
|
[Description("Push the image after building it")]
|
||||||
|
public bool Push { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BuildCommand : Command<BuldSettings>
|
||||||
|
{
|
||||||
|
public override int Execute(CommandContext context, BuldSettings settings)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Building image from {settings.Path} with tag {settings.Tag}");
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
25
Commands/Image/Module.cs
Normal file
25
Commands/Image/Module.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
using Automancer.Common;
|
||||||
|
using Spectre.Console.Cli;
|
||||||
|
|
||||||
|
namespace Automancer.Command.Image;
|
||||||
|
|
||||||
|
public class Module : ICommandModuleWithRegistry
|
||||||
|
{
|
||||||
|
public void Configure(IConfigurator config)
|
||||||
|
{
|
||||||
|
// No implementation needed here
|
||||||
|
}
|
||||||
|
public void Configure(IConfigurator config, CommandRegistry registry)
|
||||||
|
{
|
||||||
|
config.AddBranch("image", image => {
|
||||||
|
var description = "Docker/podman image operations";
|
||||||
|
image.SetDescription(description);
|
||||||
|
registry.Add("image", description);
|
||||||
|
|
||||||
|
image.AddCommand<BuildCommand>("build").WithDescription("Build a docker image");
|
||||||
|
image.AddCommand<PushCommand>("push").WithDescription("Push a docker image");
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
30
Commands/Image/PushCommand.cs
Normal file
30
Commands/Image/PushCommand.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using Spectre.Console.Cli;
|
||||||
|
|
||||||
|
namespace Automancer.Command.Image;
|
||||||
|
|
||||||
|
public class PushSettings: CommandSettings
|
||||||
|
{
|
||||||
|
[CommandArgument(0, "[path]")]
|
||||||
|
[Description("Path to the Dockerfile")]
|
||||||
|
public string? Path { get; set; }
|
||||||
|
|
||||||
|
[CommandOption("-t|--tag")]
|
||||||
|
[Description("Tag to use for the image")]
|
||||||
|
public string? Tag { get; set; }
|
||||||
|
|
||||||
|
[CommandOption("-f|--file")]
|
||||||
|
[Description("Path to the Dockerfile")]
|
||||||
|
public string? File { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PushCommand : Command<BuldSettings>
|
||||||
|
{
|
||||||
|
public override int Execute(CommandContext context, BuldSettings settings)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Building image from {settings.Path} with tag {settings.Tag}");
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
38
Commands/RootCommand.cs
Normal file
38
Commands/RootCommand.cs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
using Automancer.Common;
|
||||||
|
using Spectre.Console;
|
||||||
|
using Spectre.Console.Cli;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
|
namespace Automancer.Commands;
|
||||||
|
|
||||||
|
public class RootCommand : Spectre.Console.Cli.Command
|
||||||
|
{
|
||||||
|
private readonly CommandRegistry _registry;
|
||||||
|
|
||||||
|
public RootCommand(CommandRegistry registry)
|
||||||
|
{
|
||||||
|
_registry = registry;
|
||||||
|
}
|
||||||
|
public override int Execute([NotNull] CommandContext context)
|
||||||
|
{
|
||||||
|
AnsiConsole.Write(
|
||||||
|
new FigletText("AUTOMANCER")
|
||||||
|
.Centered()
|
||||||
|
.Color(Color.Cyan1));
|
||||||
|
|
||||||
|
AnsiConsole.MarkupLine("[bold]Automancer[/] is a modular CLI automation tool. It is designed to be extensible and easy to use.");
|
||||||
|
AnsiConsole.MarkupLine("Use [green]--help[/] or [green]<command> --help[/] to get started.");
|
||||||
|
|
||||||
|
var table = new Table().Border(TableBorder.Rounded);
|
||||||
|
table.AddColumn("[bold yellow]Module[/]");
|
||||||
|
table.AddColumn("[bold yellow]Description[/]");
|
||||||
|
|
||||||
|
foreach (var cmd in _registry.Commands.OrderBy(c => c.Path))
|
||||||
|
{
|
||||||
|
table.AddRow($"[green]{cmd.Path}[/]", cmd.Description ?? "[dim]n/a[/]");
|
||||||
|
}
|
||||||
|
|
||||||
|
AnsiConsole.Write(table);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
13
Common/CommandRegistry.cs
Normal file
13
Common/CommandRegistry.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
namespace Automancer.Common;
|
||||||
|
|
||||||
|
public record CommandInfo(string Path, string? Description);
|
||||||
|
|
||||||
|
public class CommandRegistry
|
||||||
|
{
|
||||||
|
public List<CommandInfo> Commands { get; } = new();
|
||||||
|
|
||||||
|
public void Add(string path, string? description)
|
||||||
|
{
|
||||||
|
Commands.Add(new CommandInfo(path, description));
|
||||||
|
}
|
||||||
|
}
|
15
Common/ICommandModule.cs
Normal file
15
Common/ICommandModule.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
namespace Automancer.Common;
|
||||||
|
|
||||||
|
using Spectre.Console.Cli;
|
||||||
|
|
||||||
|
public interface ICommandModule
|
||||||
|
{
|
||||||
|
|
||||||
|
void Configure(IConfigurator config);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ICommandModuleWithRegistry : ICommandModule
|
||||||
|
{
|
||||||
|
void Configure(IConfigurator config, CommandRegistry registry);
|
||||||
|
}
|
34
Common/TypeRegistrar.cs
Normal file
34
Common/TypeRegistrar.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Spectre.Console.Cli;
|
||||||
|
|
||||||
|
namespace Automancer.Common;
|
||||||
|
|
||||||
|
public sealed class TypeRegistrar : ITypeRegistrar
|
||||||
|
{
|
||||||
|
private readonly IServiceCollection _builder;
|
||||||
|
|
||||||
|
public TypeRegistrar(IServiceCollection builder)
|
||||||
|
{
|
||||||
|
_builder = builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ITypeResolver Build()
|
||||||
|
{
|
||||||
|
return new TypeResolver(_builder.BuildServiceProvider());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Register(Type service, Type implementation)
|
||||||
|
{
|
||||||
|
_builder.AddSingleton(service, implementation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterInstance(Type service, object implementation)
|
||||||
|
{
|
||||||
|
_builder.AddSingleton(service, implementation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterLazy(Type service, Func<object> factory)
|
||||||
|
{
|
||||||
|
_builder.AddSingleton(service, _ => factory());
|
||||||
|
}
|
||||||
|
}
|
24
Common/TypeResolver.cs
Normal file
24
Common/TypeResolver.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Spectre.Console.Cli;
|
||||||
|
|
||||||
|
namespace Automancer.Common;
|
||||||
|
|
||||||
|
public sealed class TypeResolver : ITypeResolver, IDisposable
|
||||||
|
{
|
||||||
|
private readonly ServiceProvider _provider;
|
||||||
|
|
||||||
|
public TypeResolver(ServiceProvider provider)
|
||||||
|
{
|
||||||
|
_provider = provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? Resolve(Type? type)
|
||||||
|
{
|
||||||
|
return type is null ? null : _provider.GetService(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_provider.Dispose();
|
||||||
|
}
|
||||||
|
}
|
99
Program.cs
99
Program.cs
@ -1,2 +1,97 @@
|
|||||||
// See https://aka.ms/new-console-template for more information
|
namespace Automancer;
|
||||||
Console.WriteLine("Hello, World!");
|
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Automancer.Common;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Spectre.Console.Cli;
|
||||||
|
using Spectre.Console;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
|
using Automancer.Commands;
|
||||||
|
|
||||||
|
public abstract class Program
|
||||||
|
{
|
||||||
|
public static async Task<int> Main(string[] args)
|
||||||
|
{
|
||||||
|
CommandApp? app = null;
|
||||||
|
|
||||||
|
var host = Host.CreateDefaultBuilder(args)
|
||||||
|
.ConfigureLogging(logging =>
|
||||||
|
{
|
||||||
|
logging.ClearProviders();
|
||||||
|
logging.AddConsole();
|
||||||
|
logging.SetMinimumLevel(LogLevel.Information);
|
||||||
|
})
|
||||||
|
.ConfigureAppConfiguration((hostingContext, config) => {
|
||||||
|
var env = hostingContext.HostingEnvironment;
|
||||||
|
|
||||||
|
config.SetBasePath(AppContext.BaseDirectory)
|
||||||
|
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
|
||||||
|
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true)
|
||||||
|
.AddEnvironmentVariables(prefix: "AUTOMANCER_")
|
||||||
|
.AddCommandLine(args);
|
||||||
|
})
|
||||||
|
.ConfigureServices((context, services) =>
|
||||||
|
{
|
||||||
|
var registry = new CommandRegistry();
|
||||||
|
services.AddSingleton(registry);
|
||||||
|
|
||||||
|
RegisterCommandModules(services);
|
||||||
|
|
||||||
|
var registrar = new TypeRegistrar(services);
|
||||||
|
app = new CommandApp(registrar);
|
||||||
|
app.SetDefaultCommand<RootCommand>();
|
||||||
|
|
||||||
|
var provider = services.BuildServiceProvider();
|
||||||
|
|
||||||
|
app.Configure(config =>
|
||||||
|
{
|
||||||
|
config.SetApplicationName("automancer");
|
||||||
|
|
||||||
|
var modules = provider.GetServices<ICommandModule>();
|
||||||
|
var registry = provider.GetRequiredService<CommandRegistry>();
|
||||||
|
|
||||||
|
foreach (var module in modules)
|
||||||
|
{
|
||||||
|
if (module is ICommandModuleWithRegistry withRegistry)
|
||||||
|
{
|
||||||
|
withRegistry.Configure(config, registry);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
module.Configure(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
return app != null ? await app.RunAsync(args) : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RegisterCommandModules(IServiceCollection services)
|
||||||
|
{
|
||||||
|
var moduleType = typeof(ICommandModule);
|
||||||
|
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||||
|
|
||||||
|
var moduleTypes = assemblies
|
||||||
|
.SelectMany(a =>
|
||||||
|
{
|
||||||
|
try { return a.GetTypes(); }
|
||||||
|
catch { return []; }
|
||||||
|
})
|
||||||
|
.Where(t => moduleType.IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var type in moduleTypes)
|
||||||
|
{
|
||||||
|
services.AddSingleton(type);
|
||||||
|
services.AddSingleton(typeof(ICommandModule), type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user