using Sdt.Config; using Sdt.Core; using Sdt.Bridge; using Sdt.Tui; using Spectre.Console; try { var cliArgs = Environment.GetCommandLineArgs().Skip(1).ToArray(); if (TryGetWorkspaceCommand(cliArgs, out var workspaceCommand)) return RunWorkspaceCommand(cliArgs, workspaceCommand); if (TryGetBridgeCommand(cliArgs, out var bridgeCommand)) return await RunBridgeCommandAsync(cliArgs, bridgeCommand); if (TryGetHeadlessCommand(cliArgs, out var headlessKind)) { var exit = await RunHeadlessAsync(cliArgs, headlessKind); return exit; } // ── Workspace + project discovery ──────────────────────────────────────── var workspaceResult = WorkspaceLoader.FindAndLoad(); var projectResult = ConfigLoader.FindAndLoad(); var forceInit = cliArgs.Any(a => string.Equals(a, "init", StringComparison.OrdinalIgnoreCase) || string.Equals(a, "--init", StringComparison.OrdinalIgnoreCase)); if (forceInit) { var scan = ConfigBootstrapper.Scan(Directory.GetCurrentDirectory()); var generated = ConfigBootstrapper.BuildDefaultConfig(scan); var path = ConfigBootstrapper.WriteDefaultConfig(scan.ProjectRoot, generated, overwrite: false); AnsiConsole.MarkupLine(Theme.Ok($"Initialized config at {path}")); projectResult = ConfigLoader.FindAndLoad(scan.ProjectRoot); } if (projectResult is null) { AnsiConsole.MarkupLine($"[bold {Theme.Red}]SDT:[/] [{Theme.Amber}]No devtool.json found[/] in current directory or any parent."); var bootstrap = AnsiConsole.Confirm( $"[{Theme.Amber}]Generate a default devtool.json for this project now?[/]", defaultValue: true); if (!bootstrap) { AnsiConsole.MarkupLine(Theme.Faint("Create a devtool.json in your project root to get started.")); return 1; } var scan = ConfigBootstrapper.Scan(Directory.GetCurrentDirectory()); AnsiConsole.MarkupLine(Theme.Faint($"Detected project root: {scan.ProjectRoot}")); if (scan.ToolFamilies.Count > 0) AnsiConsole.MarkupLine(Theme.Faint($"Detected tool families: {string.Join(", ", scan.ToolFamilies)}")); var generated = ConfigBootstrapper.BuildDefaultConfig(scan); var preview = ConfigBootstrapper.ToJson(generated); AnsiConsole.Write(new Panel(Markup.Escape(preview)).Header("Generated devtool.json preview").BorderStyle(Theme.DimStyle)); var confirmWrite = AnsiConsole.Confirm( $"[{Theme.Amber}]Write generated devtool.json to {scan.ProjectRoot}?[/]", defaultValue: true); if (!confirmWrite) return 1; var path = ConfigBootstrapper.WriteDefaultConfig(scan.ProjectRoot, generated, overwrite: false); AnsiConsole.MarkupLine(Theme.Ok($"Created {path}")); projectResult = ConfigLoader.FindAndLoad(scan.ProjectRoot); if (projectResult is null) { AnsiConsole.MarkupLine(Theme.Fail("Generated config could not be reloaded.")); return 1; } } // ── Main run loop (handles workspace project switching) ──────────────── var currentLoaded = projectResult; var (workspace, workspaceRoot) = workspaceResult.HasValue ? (workspaceResult.Value.Config, workspaceResult.Value.WorkspaceRoot) : ((WorkspaceConfig?)null, (string?)null); string? pendingWorkflowId = null; while (true) { var app = new App( currentLoaded.Config, currentLoaded.ProjectRoot, currentLoaded.Warnings, workspace, workspaceRoot, pendingWorkflowId); pendingWorkflowId = null; var result = await app.RunAsync(); if (result.Reason == AppExitReason.Quit) break; if (result.Reason == AppExitReason.SwitchProject && result.NewProjectRoot is not null) { LoadedProjectConfig? loaded; try { loaded = ConfigLoader.FindAndLoad(result.NewProjectRoot); } catch (Exception ex) { AnsiConsole.MarkupLine(Theme.Fail(ex.Message)); AnsiConsole.MarkupLine(Theme.Faint("Press any key to stay on current project...")); Console.ReadKey(intercept: true); continue; } if (loaded is null) { AnsiConsole.MarkupLine(Theme.Fail($"No devtool.json found at: {result.NewProjectRoot}")); AnsiConsole.MarkupLine(Theme.Faint("Press any key to stay on current project...")); Console.ReadKey(intercept: true); continue; } currentLoaded = loaded; pendingWorkflowId = result.RunWorkflowId; } } return 0; } catch (Exception ex) { var message = ex.Message; var isExpectedMigrationError = ex is InvalidOperationException && message.Contains("Legacy targets-only config detected", StringComparison.OrdinalIgnoreCase); AnsiConsole.MarkupLine(Theme.Fail($"Fatal: {message}")); if (isExpectedMigrationError) { var configPath = ConfigLoader.FindConfigPath(); if (!string.IsNullOrWhiteSpace(configPath)) { var migrate = AnsiConsole.Confirm( $"[{Theme.Amber}]Apply automatic migration now (creates backup + converts targets -> workflows)?[/]", defaultValue: true); if (migrate) { var result = ConfigLoader.ApplyLegacyTargetMigration(configPath, createBackup: true); if (result.Success) { AnsiConsole.MarkupLine(Theme.Ok("Migration applied successfully.")); if (!string.IsNullOrWhiteSpace(result.BackupPath)) AnsiConsole.MarkupLine(Theme.Faint($"Backup: {result.BackupPath}")); AnsiConsole.MarkupLine(Theme.Faint("Run sdt.exe again in strict mode.")); } else { AnsiConsole.MarkupLine(Theme.Fail($"Migration failed: {result.Message}")); } } } } if (!isExpectedMigrationError) AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything); return 1; } static bool TryGetHeadlessCommand(IReadOnlyList cliArgs, out string kind) { kind = ""; if (cliArgs.Count == 0) return false; if (string.Equals(cliArgs[0], "run", StringComparison.OrdinalIgnoreCase)) { kind = "run"; return true; } if (string.Equals(cliArgs[0], "debug", StringComparison.OrdinalIgnoreCase)) { kind = "debug"; return true; } return false; } static bool TryGetWorkspaceCommand(IReadOnlyList cliArgs, out string command) { command = ""; if (cliArgs.Count < 2) return false; if (!string.Equals(cliArgs[0], "workspace", StringComparison.OrdinalIgnoreCase)) return false; if (string.Equals(cliArgs[1], "scan", StringComparison.OrdinalIgnoreCase)) { command = "scan"; return true; } return false; } static bool TryGetBridgeCommand(IReadOnlyList cliArgs, out string command) { command = ""; if (cliArgs.Count < 2) return false; if (!string.Equals(cliArgs[0], "bridge", StringComparison.OrdinalIgnoreCase)) return false; if (string.Equals(cliArgs[1], "--stdio", StringComparison.OrdinalIgnoreCase)) { command = "stdio"; return true; } return false; } static async Task RunBridgeCommandAsync(IReadOnlyList cliArgs, string command) { if (!string.Equals(command, "stdio", StringComparison.OrdinalIgnoreCase)) return ExitCodeMapper.FromResult(false, ExecutionStopReason.ValidationFailed); var options = ParseOptions(cliArgs.Skip(2).ToArray(), out _); var startDir = options.TryGetValue("--project-root", out var root) && !string.IsNullOrWhiteSpace(root) ? root : Directory.GetCurrentDirectory(); var server = new BridgeStdioServer(startDir); return await server.RunAsync(); } static int RunWorkspaceCommand(IReadOnlyList cliArgs, string command) { if (!string.Equals(command, "scan", StringComparison.OrdinalIgnoreCase)) return ExitCodeMapper.FromResult(false, ExecutionStopReason.ValidationFailed); var options = ParseOptions(cliArgs.Skip(2).ToArray(), out _); var startDir = options.TryGetValue("--project-root", out var root) && !string.IsNullOrWhiteSpace(root) ? root : Directory.GetCurrentDirectory(); var asJson = options.ContainsKey("--json"); var workspaceLoaded = WorkspaceLoader.FindAndLoad(startDir); if (workspaceLoaded is null) { var payload = new { success = false, message = "No workspace could be discovered." }; Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(payload)); return ExitCodeMapper.FromResult(false, ExecutionStopReason.ValidationFailed); } var (workspace, workspaceRoot) = workspaceLoaded.Value; var currentRoot = ConfigLoader.FindAndLoad(startDir)?.ProjectRoot ?? startDir; var scan = WorkspaceLoader.ScanInventory(workspaceRoot, currentRoot, workspace); if (asJson) { Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(scan, new System.Text.Json.JsonSerializerOptions { PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase, WriteIndented = true }.WithEnumStrings())); } else { Console.WriteLine($"Workspace root: {scan.WorkspaceRoot}"); Console.WriteLine($"Known projects: {scan.KnownProjects.Count}"); Console.WriteLine($"Candidates: {scan.Candidates.Count}"); foreach (var item in scan.Candidates) Console.WriteLine($" - {item.DisplayName} [{item.PrimaryKind}] {item.RootPath}"); } return 0; } static async Task RunHeadlessAsync(IReadOnlyList cliArgs, string kind) { var options = ParseOptions(cliArgs.Skip(1).ToArray(), out var positional); var json = options.ContainsKey("--json"); var startDir = options.TryGetValue("--project-root", out var projectRootOpt) ? projectRootOpt : Directory.GetCurrentDirectory(); var envProfile = options.TryGetValue("--env-profile", out var profile) ? profile : null; var nonInteractive = options.ContainsKey("--non-interactive") || RuntimePolicy.IsNonInteractive(); var loaded = ConfigLoader.FindAndLoad(startDir); if (loaded is null) { Console.WriteLine("{\"success\":false,\"message\":\"No devtool.json found for headless command.\"}"); return ExitCodeMapper.FromResult(false, ExecutionStopReason.ValidationFailed); } var service = new HeadlessExecutionService(); if (kind == "run") { if (positional.Count == 0) { Console.WriteLine("{\"success\":false,\"message\":\"Missing workflow id. Usage: sdt run [--json] [--project-root ] [--env-profile ] [--non-interactive]\"}"); return ExitCodeMapper.FromResult(false, ExecutionStopReason.ValidationFailed); } return await service.RunWorkflowAsync( loaded, new HeadlessRunRequest( WorkflowId: positional[0], ProjectRoot: loaded.ProjectRoot, EnvProfile: envProfile, NonInteractive: nonInteractive, JsonOutput: json)); } if (positional.Count == 0) { Console.WriteLine("{\"success\":false,\"message\":\"Missing debug profile id. Usage: sdt debug [--json] [--project-root ] [--env-profile ] [--non-interactive]\"}"); return ExitCodeMapper.FromResult(false, ExecutionStopReason.ValidationFailed); } return await service.RunDebugAsync( loaded, new HeadlessDebugRequest( ProfileId: positional[0], ProjectRoot: loaded.ProjectRoot, EnvProfile: envProfile, NonInteractive: nonInteractive, JsonOutput: json)); } static Dictionary ParseOptions(string[] args, out List positional) { var options = new Dictionary(StringComparer.OrdinalIgnoreCase); positional = []; for (var i = 0; i < args.Length; i++) { var token = args[i]; if (!token.StartsWith("-", StringComparison.Ordinal)) { positional.Add(token); continue; } if (i + 1 < args.Length && !args[i + 1].StartsWith("-", StringComparison.Ordinal)) { options[token] = args[i + 1]; i++; } else { options[token] = null; } } return options; } static class JsonOptionsExtensions { public static System.Text.Json.JsonSerializerOptions WithEnumStrings(this System.Text.Json.JsonSerializerOptions options) { options.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter()); return options; } }