using System.CommandLine; using NatsNet.PortTracker.Data; namespace NatsNet.PortTracker.Commands; public static class BatchCommands { public static Command Create(Option dbOption) { var batchCommand = new Command("batch", "Manage implementation batches"); batchCommand.Add(CreateList(dbOption)); batchCommand.Add(CreateShow(dbOption)); batchCommand.Add(CreateReady(dbOption)); batchCommand.Add(CreateNext(dbOption)); batchCommand.Add(CreateStart(dbOption)); batchCommand.Add(CreateComplete(dbOption)); return batchCommand; } private static Command CreateList(Option dbOption) { var statusOpt = new Option("--status") { Description = "Filter by status: pending, in_progress, complete" }; var cmd = new Command("list", "List all implementation batches"); cmd.Add(statusOpt); cmd.SetAction(parseResult => { var dbPath = parseResult.GetValue(dbOption)!; var status = parseResult.GetValue(statusOpt); using var db = new Database(dbPath); var sql = "SELECT id, name, feature_count, test_count, status, depends_on FROM implementation_batches"; var parameters = new List<(string, object?)>(); if (status is not null) { sql += " WHERE status = @status"; parameters.Add(("@status", status)); } sql += " ORDER BY priority, id"; var rows = db.Query(sql, parameters.ToArray()); if (rows.Count == 0) { Console.WriteLine("No batches found."); return; } Console.WriteLine($"{"ID",-4} {"Name",-45} {"Feat",-5} {"Test",-5} {"Status",-12} {"Depends On"}"); Console.WriteLine(new string('-', 100)); foreach (var row in rows) { Console.WriteLine( $"{row["id"],-4} " + $"{Truncate(row["name"]?.ToString(), 44),-45} " + $"{row["feature_count"],-5} " + $"{row["test_count"],-5} " + $"{row["status"],-12} " + $"{row["depends_on"] ?? ""}"); } Console.WriteLine($"\nTotal: {rows.Count} batches"); }); return cmd; } private static Command CreateShow(Option dbOption) { var idArg = new Argument("id") { Description = "Batch ID to show" }; var cmd = new Command("show", "Show batch details with features and tests"); cmd.Add(idArg); cmd.SetAction(parseResult => { var dbPath = parseResult.GetValue(dbOption)!; var id = parseResult.GetValue(idArg); using var db = new Database(dbPath); var batches = db.Query( "SELECT * FROM implementation_batches WHERE id = @id", ("@id", id)); if (batches.Count == 0) { Console.WriteLine($"Batch {id} not found."); return; } var batch = batches[0]; Console.WriteLine($"Batch {batch["id"]}: {batch["name"]}"); Console.WriteLine($" Description: {batch["description"] ?? "(none)"}"); Console.WriteLine($" Priority: {batch["priority"]}"); Console.WriteLine($" Status: {batch["status"]}"); Console.WriteLine($" Depends On: {batch["depends_on"] ?? "(none)"}"); Console.WriteLine($" Go Files: {batch["go_files"] ?? "(none)"}"); Console.WriteLine($" Features: {batch["feature_count"]}"); Console.WriteLine($" Tests: {batch["test_count"]}"); Console.WriteLine(); // Show features var features = db.Query( """ SELECT f.id, f.name, f.status, f.go_file FROM batch_features bf JOIN features f ON f.id = bf.feature_id WHERE bf.batch_id = @id ORDER BY f.id """, ("@id", id)); if (features.Count > 0) { Console.WriteLine($"Features ({features.Count}):"); Console.WriteLine($" {"ID",-6} {"Status",-10} {"Name",-50} {"Go File"}"); Console.WriteLine(" " + new string('-', 90)); foreach (var f in features) { Console.WriteLine( $" {f["id"],-6} " + $"{f["status"],-10} " + $"{Truncate(f["name"]?.ToString(), 49),-50} " + $"{Truncate(f["go_file"]?.ToString(), 30)}"); } } Console.WriteLine(); // Show tests var tests = db.Query( """ SELECT t.id, t.name, t.status, t.go_file FROM batch_tests bt JOIN unit_tests t ON t.id = bt.test_id WHERE bt.batch_id = @id ORDER BY t.id """, ("@id", id)); if (tests.Count > 0) { Console.WriteLine($"Tests ({tests.Count}):"); Console.WriteLine($" {"ID",-6} {"Status",-10} {"Name",-50} {"Go File"}"); Console.WriteLine(" " + new string('-', 90)); foreach (var t in tests) { Console.WriteLine( $" {t["id"],-6} " + $"{t["status"],-10} " + $"{Truncate(t["name"]?.ToString(), 49),-50} " + $"{Truncate(t["go_file"]?.ToString(), 30)}"); } } }); return cmd; } private static Command CreateReady(Option dbOption) { var cmd = new Command("ready", "List batches ready to start (all dependencies complete)"); cmd.SetAction(parseResult => { var dbPath = parseResult.GetValue(dbOption)!; using var db = new Database(dbPath); var readyBatches = GetReadyBatches(db); if (readyBatches.Count == 0) { Console.WriteLine("No batches ready."); return; } Console.WriteLine($"{"ID",-4} {"Name",-45} {"Feat",-5} {"Test",-5} {"Priority"}"); Console.WriteLine(new string('-', 70)); foreach (var row in readyBatches) { Console.WriteLine( $"{row["id"],-4} " + $"{Truncate(row["name"]?.ToString(), 44),-45} " + $"{row["feature_count"],-5} " + $"{row["test_count"],-5} " + $"{row["priority"]}"); } Console.WriteLine($"\n{readyBatches.Count} batches ready"); }); return cmd; } private static Command CreateNext(Option dbOption) { var cmd = new Command("next", "Show the next recommended batch (lowest priority ready)"); cmd.SetAction(parseResult => { var dbPath = parseResult.GetValue(dbOption)!; using var db = new Database(dbPath); var readyBatches = GetReadyBatches(db); if (readyBatches.Count == 0) { Console.WriteLine("No batches ready."); return; } var next = readyBatches[0]; // Already sorted by priority Console.WriteLine($"Next batch: #{next["id"]} — {next["name"]}"); Console.WriteLine($" Priority: {next["priority"]}"); Console.WriteLine($" Features: {next["feature_count"]}"); Console.WriteLine($" Tests: {next["test_count"]}"); Console.WriteLine($" Depends On: {next["depends_on"] ?? "(none)"}"); Console.WriteLine($"\nRun: batch start {next["id"]}"); }); return cmd; } private static Command CreateStart(Option dbOption) { var idArg = new Argument("id") { Description = "Batch ID to start" }; var cmd = new Command("start", "Mark batch as in-progress (validates dependencies)"); cmd.Add(idArg); cmd.SetAction(parseResult => { var dbPath = parseResult.GetValue(dbOption)!; var id = parseResult.GetValue(idArg); using var db = new Database(dbPath); var batches = db.Query( "SELECT * FROM implementation_batches WHERE id = @id", ("@id", id)); if (batches.Count == 0) { Console.WriteLine($"Batch {id} not found."); return; } var batch = batches[0]; var status = batch["status"]?.ToString(); if (status == "complete") { Console.WriteLine($"Batch {id} is already complete."); return; } if (status == "in_progress") { Console.WriteLine($"Batch {id} is already in progress."); return; } // Check dependencies if (!AreDependenciesMet(db, batch)) { var deps = batch["depends_on"]?.ToString() ?? ""; Console.WriteLine($"Cannot start batch {id}: dependencies not met."); Console.WriteLine($" Depends on: {deps}"); PrintDependencyStatus(db, deps); return; } db.Execute( "UPDATE implementation_batches SET status = 'in_progress' WHERE id = @id", ("@id", id)); Console.WriteLine($"Batch {id} started: {batch["name"]}"); Console.WriteLine($" Features: {batch["feature_count"]}, Tests: {batch["test_count"]}"); Console.WriteLine($"\nRun: batch show {id} — to see all items"); }); return cmd; } private static Command CreateComplete(Option dbOption) { var idArg = new Argument("id") { Description = "Batch ID to complete" }; var cmd = new Command("complete", "Mark batch as complete (validates all items done)"); cmd.Add(idArg); cmd.SetAction(parseResult => { var dbPath = parseResult.GetValue(dbOption)!; var id = parseResult.GetValue(idArg); using var db = new Database(dbPath); var batches = db.Query( "SELECT * FROM implementation_batches WHERE id = @id", ("@id", id)); if (batches.Count == 0) { Console.WriteLine($"Batch {id} not found."); return; } var batch = batches[0]; if (batch["status"]?.ToString() == "complete") { Console.WriteLine($"Batch {id} is already complete."); return; } // Check all features are done var incompleteFeatures = db.Query( """ SELECT f.id, f.name, f.status FROM batch_features bf JOIN features f ON f.id = bf.feature_id WHERE bf.batch_id = @id AND f.status NOT IN ('verified', 'complete', 'n_a') ORDER BY f.id """, ("@id", id)); // Check all tests are done var incompleteTests = db.Query( """ SELECT t.id, t.name, t.status FROM batch_tests bt JOIN unit_tests t ON t.id = bt.test_id WHERE bt.batch_id = @id AND t.status NOT IN ('verified', 'complete', 'n_a') ORDER BY t.id """, ("@id", id)); if (incompleteFeatures.Count > 0 || incompleteTests.Count > 0) { Console.WriteLine($"Cannot complete batch {id}: items remain."); if (incompleteFeatures.Count > 0) { Console.WriteLine($"\n Incomplete features ({incompleteFeatures.Count}):"); foreach (var f in incompleteFeatures.Take(20)) { Console.WriteLine($" {f["id"],-6} {f["status"],-10} {Truncate(f["name"]?.ToString(), 50)}"); } if (incompleteFeatures.Count > 20) Console.WriteLine($" ... and {incompleteFeatures.Count - 20} more"); } if (incompleteTests.Count > 0) { Console.WriteLine($"\n Incomplete tests ({incompleteTests.Count}):"); foreach (var t in incompleteTests.Take(20)) { Console.WriteLine($" {t["id"],-6} {t["status"],-10} {Truncate(t["name"]?.ToString(), 50)}"); } if (incompleteTests.Count > 20) Console.WriteLine($" ... and {incompleteTests.Count - 20} more"); } return; } db.Execute( "UPDATE implementation_batches SET status = 'complete' WHERE id = @id", ("@id", id)); Console.WriteLine($"Batch {id} completed: {batch["name"]}"); }); return cmd; } private static List> GetReadyBatches(Database db) { // Get all pending batches var pending = db.Query( "SELECT * FROM implementation_batches WHERE status = 'pending' ORDER BY priority, id"); // Get statuses of all batches for dependency checking var allStatuses = db.Query("SELECT id, status FROM implementation_batches"); var statusMap = new Dictionary(); foreach (var row in allStatuses) { statusMap[(long)row["id"]!] = row["status"]?.ToString() ?? "pending"; } var ready = new List>(); foreach (var batch in pending) { if (AreDependenciesMet(statusMap, batch["depends_on"]?.ToString())) ready.Add(batch); } return ready; } private static bool AreDependenciesMet(Database db, Dictionary batch) { var depsStr = batch["depends_on"]?.ToString(); if (string.IsNullOrWhiteSpace(depsStr)) return true; var allStatuses = db.Query("SELECT id, status FROM implementation_batches"); var statusMap = new Dictionary(); foreach (var row in allStatuses) { statusMap[(long)row["id"]!] = row["status"]?.ToString() ?? "pending"; } return AreDependenciesMet(statusMap, depsStr); } private static bool AreDependenciesMet(Dictionary statusMap, string? depsStr) { if (string.IsNullOrWhiteSpace(depsStr)) return true; var depIds = depsStr.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); foreach (var depIdStr in depIds) { if (long.TryParse(depIdStr, out var depId)) { if (!statusMap.TryGetValue(depId, out var depStatus) || depStatus != "complete") return false; } } return true; } private static void PrintDependencyStatus(Database db, string depsStr) { if (string.IsNullOrWhiteSpace(depsStr)) return; var depIds = depsStr.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); foreach (var depIdStr in depIds) { if (int.TryParse(depIdStr, out var depId)) { var rows = db.Query( "SELECT id, name, status FROM implementation_batches WHERE id = @id", ("@id", depId)); if (rows.Count > 0) { var r = rows[0]; Console.WriteLine($" Batch {r["id"]}: {r["status"]} — {r["name"]}"); } } } } private static string Truncate(string? s, int maxLen) { if (s is null) return ""; return s.Length <= maxLen ? s : s[..(maxLen - 2)] + ".."; } }