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
|
||||
.DS_Store
|
||||
.vscode
|
||||
.idea
|
||||
|
@ -8,7 +8,11 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<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>
|
||||
|
||||
</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
|
||||
Console.WriteLine("Hello, World!");
|
||||
namespace Automancer;
|
||||
|
||||
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