diff --git a/.gitignore b/.gitignore
index bd67d36..e4a167e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -402,3 +402,4 @@ FodyWeavers.xsd
# End of https://www.toptal.com/developers/gitignore/api/csharp
.DS_Store
.vscode
+.idea
diff --git a/Automancer.csproj b/Automancer.csproj
index e3fb451..f4d5391 100644
--- a/Automancer.csproj
+++ b/Automancer.csproj
@@ -8,7 +8,11 @@
-
+
+
+
+
+
diff --git a/Commands/Container/Module.cs b/Commands/Container/Module.cs
new file mode 100644
index 0000000..5599757
--- /dev/null
+++ b/Commands/Container/Module.cs
@@ -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("ps").WithDescription("List containers");
+ });
+ }
+}
\ No newline at end of file
diff --git a/Commands/Container/PsCommand.cs b/Commands/Container/PsCommand.cs
new file mode 100644
index 0000000..ef095c4
--- /dev/null
+++ b/Commands/Container/PsCommand.cs
@@ -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
+{
+ public override int Execute(CommandContext context, BuldSettings settings)
+ {
+ Console.WriteLine($"Building image from {settings.Path} with tag {settings.Tag}");
+
+ return 0;
+ }
+}
+
diff --git a/Commands/Image/BuildCommand.cs b/Commands/Image/BuildCommand.cs
new file mode 100644
index 0000000..1c22d32
--- /dev/null
+++ b/Commands/Image/BuildCommand.cs
@@ -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
+{
+ public override int Execute(CommandContext context, BuldSettings settings)
+ {
+ Console.WriteLine($"Building image from {settings.Path} with tag {settings.Tag}");
+
+ return 0;
+ }
+}
+
diff --git a/Commands/Image/Module.cs b/Commands/Image/Module.cs
new file mode 100644
index 0000000..111d209
--- /dev/null
+++ b/Commands/Image/Module.cs
@@ -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("build").WithDescription("Build a docker image");
+ image.AddCommand("push").WithDescription("Push a docker image");
+
+ });
+ }
+
+}
\ No newline at end of file
diff --git a/Commands/Image/PushCommand.cs b/Commands/Image/PushCommand.cs
new file mode 100644
index 0000000..0108e7d
--- /dev/null
+++ b/Commands/Image/PushCommand.cs
@@ -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
+{
+ public override int Execute(CommandContext context, BuldSettings settings)
+ {
+ Console.WriteLine($"Building image from {settings.Path} with tag {settings.Tag}");
+
+ return 0;
+ }
+}
+
diff --git a/Commands/RootCommand.cs b/Commands/RootCommand.cs
new file mode 100644
index 0000000..96c4e3b
--- /dev/null
+++ b/Commands/RootCommand.cs
@@ -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] --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;
+ }
+}
diff --git a/Common/CommandRegistry.cs b/Common/CommandRegistry.cs
new file mode 100644
index 0000000..3c4a611
--- /dev/null
+++ b/Common/CommandRegistry.cs
@@ -0,0 +1,13 @@
+namespace Automancer.Common;
+
+public record CommandInfo(string Path, string? Description);
+
+public class CommandRegistry
+{
+ public List Commands { get; } = new();
+
+ public void Add(string path, string? description)
+ {
+ Commands.Add(new CommandInfo(path, description));
+ }
+}
diff --git a/Common/ICommandModule.cs b/Common/ICommandModule.cs
new file mode 100644
index 0000000..1fadae4
--- /dev/null
+++ b/Common/ICommandModule.cs
@@ -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);
+}
\ No newline at end of file
diff --git a/Common/TypeRegistrar.cs b/Common/TypeRegistrar.cs
new file mode 100644
index 0000000..103d6ce
--- /dev/null
+++ b/Common/TypeRegistrar.cs
@@ -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