diff --git a/BugMine.CLI/BugMine.CLI.csproj b/BugMine.CLI/BugMine.CLI.csproj
new file mode 100644
index 0000000..f07ebda
--- /dev/null
+++ b/BugMine.CLI/BugMine.CLI.csproj
@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk.Worker">
+
+ <PropertyGroup>
+ <TargetFramework>net8.0</TargetFramework>
+ <Nullable>enable</Nullable>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <UserSecretsId>dotnet-BugMine.DevTools.CLI-68C1536A-FE5E-4724-8406-B1B1430F105A</UserSecretsId>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0"/>
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\BugMine.Sdk\BugMine.Sdk.csproj" />
+ </ItemGroup>
+</Project>
diff --git a/BugMine.CLI/CLIClient.cs b/BugMine.CLI/CLIClient.cs
new file mode 100644
index 0000000..1fb0f6b
--- /dev/null
+++ b/BugMine.CLI/CLIClient.cs
@@ -0,0 +1,43 @@
+using System.Text.Json;
+using ArcaneLibs.Extensions;
+using BugMine.Web.Classes;
+using LibMatrix.Responses;
+using LibMatrix.Services;
+
+namespace BugMine.CLI;
+
+public class CLIClient(ILogger<CLIClient> logger, BugMineClient client) : BackgroundService {
+ private readonly ILogger<CLIClient> _logger = logger;
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
+
+ while (!stoppingToken.IsCancellationRequested) {
+ Console.WriteLine("""
+ 1) List all projects
+ 2) Mass create projects
+ 3) Destroy all projects
+ 4) Get room count
+ 5) Summarize all projects
+ 6) Mass create regular rooms
+
+ L) Logout
+ Q) Quit
+ """);
+
+ var input = Console.ReadKey();
+ Console.WriteLine();
+ switch (input.Key) {
+
+ case ConsoleKey.L: {
+ File.Delete("auth.json");
+ await ExecuteAsync(stoppingToken);
+ return;
+ }
+ case ConsoleKey.Q: {
+ Environment.Exit(0);
+ return;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/BugMine.CLI/Classes/BaseTUIMenu.cs b/BugMine.CLI/Classes/BaseTUIMenu.cs
new file mode 100644
index 0000000..1cf4876
--- /dev/null
+++ b/BugMine.CLI/Classes/BaseTUIMenu.cs
@@ -0,0 +1,52 @@
+namespace BugMine.CLI.Interfaces;
+
+public abstract class BaseTUIMenu(ILogger logger) {
+ public virtual Dictionary<string, Func<Task>> MenuItems { get; set; }
+
+ public virtual async Task Execute() { }
+
+ public void HandleMenu() {
+ if (MenuItems == null) {
+ logger.LogCritical("Menu {type} had no MenuItems intialised!", this.GetType().FullName);
+ Environment.Exit(1);
+ }
+
+ int currentIndex = 0;
+ bool running = true;
+ // int startHeight = Console.CursorTop;
+ // Console.WriteLine("sh - " + startHeight);
+ while (running) {
+ Console.CursorLeft = 0;
+
+ // Console.CursorTop = startHeight;
+ Console.Beep();
+ int i = 0;
+ foreach (var (key, value) in MenuItems) {
+ Console.Beep();
+ Thread.Sleep(25);
+ Console.WriteLine($"{(i++ == currentIndex ? ">" : " ")} {key} " + Console.CursorTop);
+ }
+
+ Console.CursorTop -= MenuItems.Count - currentIndex;
+ var oldIndex = currentIndex;
+ var ckey = Console.ReadKey(true);
+ Console.Write(ckey.Key);
+ switch (ckey.Key) {
+ case ConsoleKey.DownArrow:
+ currentIndex++;
+ if (currentIndex >= MenuItems.Count()) currentIndex = MenuItems.Count() - 1;
+ Console.Write($" NEXT ENTRY: {currentIndex}");
+ break;
+ case ConsoleKey.UpArrow:
+ currentIndex--;
+ if (currentIndex < 0) currentIndex = 0;
+ Console.Write($" NEXT ENTRY: {currentIndex}");
+ break;
+ }
+
+ // Console.CursorTop -= MenuItems.Count() - oldIndex;
+ Thread.Sleep(250);
+ // Console.CursorTop -= currentIndex - 1;
+ }
+ }
+}
\ No newline at end of file
diff --git a/BugMine.CLI/InteractiveClient.cs b/BugMine.CLI/InteractiveClient.cs
new file mode 100644
index 0000000..e2fc3e4
--- /dev/null
+++ b/BugMine.CLI/InteractiveClient.cs
@@ -0,0 +1,45 @@
+using System.Text.Json;
+using ArcaneLibs.Extensions;
+using BugMine.CLI.Interfaces;
+using BugMine.CLI.TUIMenus;
+using BugMine.Web.Classes;
+using LibMatrix.Responses;
+using LibMatrix.Services;
+
+namespace BugMine.CLI;
+
+public class InteractiveClient(ILogger<InteractiveClient> logger, BugMineClient client, MainTUIMenu mainMenu) : BackgroundService {
+ private readonly ILogger<InteractiveClient> _logger = logger;
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
+ while (!stoppingToken.IsCancellationRequested) {
+ // Console.WriteLine("""
+ // 1) List all projects
+ // 2) Mass create projects
+ // 3) Destroy all projects
+ // 4) Get room count
+ // 5) Summarize all projects
+ // 6) Mass create regular rooms
+ //
+ // L) Logout
+ // Q) Quit
+ // """);
+ //
+ // var input = Console.ReadKey();
+ // Console.WriteLine();
+ // switch (input.Key) {
+ //
+ // case ConsoleKey.L: {
+ // File.Delete("auth.json");
+ // await ExecuteAsync(stoppingToken);
+ // return;
+ // }
+ // case ConsoleKey.Q: {
+ // Environment.Exit(0);
+ // return;
+ // }
+ // }
+ await mainMenu.Execute();
+ }
+ }
+}
\ No newline at end of file
diff --git a/BugMine.CLI/Program.cs b/BugMine.CLI/Program.cs
new file mode 100644
index 0000000..6471e43
--- /dev/null
+++ b/BugMine.CLI/Program.cs
@@ -0,0 +1,64 @@
+using System.Text.Json;
+using ArcaneLibs;
+using ArcaneLibs.Extensions;
+using BugMine.CLI;
+using BugMine.CLI.Interfaces;
+using BugMine.CLI.TUIMenus;
+using BugMine.Web.Classes;using LibMatrix.Responses;
+using LibMatrix.Services;
+
+var builder = Host.CreateApplicationBuilder(args);
+builder.Services.AddBugMine(new() { AppName = "BugMine CLI" });
+
+builder.Services.AddSingleton<BugMineClient>(a => {
+ var hsp = a.GetRequiredService<HomeserverProviderService>();
+ return getClient(hsp, args.Length == 0).GetAwaiter().GetResult(); // We can't use async here, so we have to block
+});
+
+if (args.Length == 0) {
+ Console.WriteLine("Starting interactive client...");
+ builder.Services.AddHostedService<InteractiveClient>();
+ // builder.Services.AddSingleton<BaseTUIMenu, MainTUIMenu>();
+ var menus = new ClassCollector<BaseTUIMenu>().ResolveFromAllAccessibleAssemblies();
+ foreach (var menu in menus) {
+ Console.WriteLine($"Adding menu {menu.Name}");
+ builder.Services.AddSingleton(menu);
+ builder.Services.AddSingleton(typeof(BaseTUIMenu), menu);
+ }
+}
+else {
+ Console.WriteLine($"Starting CLI client with {args.Length} args: {string.Join(",", args)}");
+ builder.Services.AddHostedService<CLIClient>();
+}
+
+var host = builder.Build();
+host.Run();
+
+
+async Task<LoginResponse> findAuth(HomeserverProviderService hsProvider, bool interactive = true) {
+ Console.WriteLine($"findAuth entered with hsProvider={{{hsProvider.GetHashCode()}}}, interactive={interactive}");
+ if (File.Exists("auth.json")) {
+ return JsonSerializer.Deserialize<LoginResponse>(File.ReadAllText("auth.json"));
+ }
+ else {
+ if (!interactive) {
+ Console.WriteLine("Could not locate account information. Please log in interactively or use `BugMine.CLI login <mxid> <password>`.");
+ Environment.Exit(1);
+ }
+ Console.Write("Homeserver: ");
+ var homeserver = Console.ReadLine()!;
+ Console.Write("Username: ");
+ var username = Console.ReadLine()!;
+ Console.Write("Password: ");
+ var password = Console.ReadLine()!;
+
+ var login = hsProvider.Login(homeserver, username, password).GetAwaiter().GetResult();
+ File.WriteAllText("auth.json", login.ToJson());
+ return login;
+ }
+}
+
+async Task<BugMineClient> getClient(HomeserverProviderService hsProvider, bool interactive) {
+ var auth = await findAuth(hsProvider, interactive);
+ return new BugMineClient(await hsProvider.GetAuthenticatedWithToken(auth.Homeserver, auth.AccessToken, useGeneric: true));
+}
\ No newline at end of file
diff --git a/BugMine.CLI/Properties/launchSettings.json b/BugMine.CLI/Properties/launchSettings.json
new file mode 100644
index 0000000..43460c6
--- /dev/null
+++ b/BugMine.CLI/Properties/launchSettings.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "BugMine.DevTools.CLI": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "environmentVariables": {
+ "DOTNET_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/BugMine.CLI/TUIMenus/AuthTUIMenu.cs b/BugMine.CLI/TUIMenus/AuthTUIMenu.cs
new file mode 100644
index 0000000..3636ade
--- /dev/null
+++ b/BugMine.CLI/TUIMenus/AuthTUIMenu.cs
@@ -0,0 +1,11 @@
+using BugMine.CLI.Interfaces;
+
+namespace BugMine.CLI.TUIMenus;
+
+public class AuthTUIMenu(ILogger<AuthTUIMenu> logger) : BaseTUIMenu(logger) {
+ public override async Task Execute() {
+ Console.WriteLine("meow from AuthTUIMenu");
+ HandleMenu();
+ await Task.Delay(10000);
+ }
+}
\ No newline at end of file
diff --git a/BugMine.CLI/TUIMenus/MainTUIMenu.cs b/BugMine.CLI/TUIMenus/MainTUIMenu.cs
new file mode 100644
index 0000000..b754447
--- /dev/null
+++ b/BugMine.CLI/TUIMenus/MainTUIMenu.cs
@@ -0,0 +1,23 @@
+using BugMine.CLI.Interfaces;
+
+namespace BugMine.CLI.TUIMenus;
+
+public class MainTUIMenu(ILogger<MainTUIMenu> logger, AuthTUIMenu authMenu) : BaseTUIMenu(logger) {
+ public override Dictionary<string, Func<Task>> MenuItems { get; set; } = new() {
+ { "auth", () => authMenu.Execute() },
+ { "1", () => Task.CompletedTask },
+ { "2", () => Task.CompletedTask },
+ { "3", () => Task.CompletedTask },
+ { "4", () => Task.CompletedTask },
+ { "5", () => Task.CompletedTask },
+ { "6", () => Task.CompletedTask },
+ { "L", () => Task.CompletedTask },
+ { "Q", () => Task.CompletedTask }
+ };
+
+ public override async Task Execute() {
+ Console.WriteLine("meow from MainTUIMenu");
+ HandleMenu();
+ await Task.Delay(10000);
+ }
+}
\ No newline at end of file
diff --git a/BugMine.CLI/appsettings.Development.json b/BugMine.CLI/appsettings.Development.json
new file mode 100644
index 0000000..b2dcdb6
--- /dev/null
+++ b/BugMine.CLI/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ }
+}
diff --git a/BugMine.CLI/appsettings.json b/BugMine.CLI/appsettings.json
new file mode 100644
index 0000000..b2dcdb6
--- /dev/null
+++ b/BugMine.CLI/appsettings.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ }
+}
diff --git a/BugMine.sln b/BugMine.sln
index f6d49f3..a2f6194 100644
--- a/BugMine.sln
+++ b/BugMine.sln
@@ -22,6 +22,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{E25BCB86
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.HomeserverEmulator", "LibMatrix\Tests\LibMatrix.HomeserverEmulator\LibMatrix.HomeserverEmulator.csproj", "{630BD644-B739-4876-B45C-085FF60C7269}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BugMine.CLI", "BugMine.CLI\BugMine.CLI.csproj", "{00F38C3E-324F-4D50-9BEB-16FABBDCFCEC}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -56,6 +58,10 @@ Global
{630BD644-B739-4876-B45C-085FF60C7269}.Debug|Any CPU.Build.0 = Debug|Any CPU
{630BD644-B739-4876-B45C-085FF60C7269}.Release|Any CPU.ActiveCfg = Release|Any CPU
{630BD644-B739-4876-B45C-085FF60C7269}.Release|Any CPU.Build.0 = Release|Any CPU
+ {00F38C3E-324F-4D50-9BEB-16FABBDCFCEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {00F38C3E-324F-4D50-9BEB-16FABBDCFCEC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {00F38C3E-324F-4D50-9BEB-16FABBDCFCEC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {00F38C3E-324F-4D50-9BEB-16FABBDCFCEC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{F4E241C3-0300-4B87-8707-BCBDEF1F0185} = {8F4F6BEC-0C66-486B-A21A-1C35B2EDAD33}
diff --git a/LibMatrix b/LibMatrix
-Subproject 896ee7f099f817e8cc9aba96a9db00fcce67163
+Subproject b5860ce2011b96a2919d5306445b0e8bd8408b3
|