diff --git a/src/Commands/AuthorizedCommandBase.cs b/src/Commands/AuthorizedCommandBase.cs index 44c2214..a7f1446 100644 --- a/src/Commands/AuthorizedCommandBase.cs +++ b/src/Commands/AuthorizedCommandBase.cs @@ -7,25 +7,19 @@ namespace devops.Commands; -public abstract class AuthorizedCommandBase : AsyncCommand where TSettings : CommandSettings +public abstract class AuthorizedCommandBase(IAnsiConsole console, DevOpsConfigurationAccessor devoptions) + : AsyncCommand + where TSettings : CommandSettings { - protected readonly IAnsiConsole _console; - - private readonly DevOpsConfigurationAccessor _devoptionsAccessor; + protected readonly IAnsiConsole Console = console; protected VssConnection? VssConnection; - protected AuthorizedCommandBase(IAnsiConsole console, DevOpsConfigurationAccessor devoptions) - { - _console = console; - _devoptionsAccessor = devoptions; - } - public override async Task ExecuteAsync(CommandContext context, TSettings settings) { try { - var config = _devoptionsAccessor.GetSettings(); + var config = devoptions.GetSettings(); var devopsUri = new Uri(config.CollectionUri); var deopsCreds = new VssBasicCredential(string.Empty, config.CollectionPAT); @@ -35,7 +29,7 @@ public override async Task ExecuteAsync(CommandContext context, TSettings s } catch (OptionsValidationException ex) { - _console.WriteLine("Could not connect to Azure Devops - " + ex.Message); + Console.WriteLine("Could not connect to Azure Devops - " + ex.Message); return Constants.UnauthorizedExitCode; } diff --git a/src/Commands/InitCommand.cs b/src/Commands/InitCommand.cs index e443ad1..d6aa19b 100644 --- a/src/Commands/InitCommand.cs +++ b/src/Commands/InitCommand.cs @@ -1,75 +1,28 @@ -using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using System.Text.Json; -using System.Text.Json.Nodes; +using System.Diagnostics.CodeAnalysis; using devops.Internal; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Microsoft.VisualStudio.Services.Common.CommandLine; using Spectre.Console; using Spectre.Console.Cli; namespace devops.Commands; -public class InitCommand : Command +public class InitCommand( + IAnsiConsole console, + DevOpsConfigurationStore configStore, + DevOpsConfigurationAccessor configurationAccessor) + : Command { - private readonly IAnsiConsole _console; - - private readonly DevOpsConfigurationProtector _protector; - - private readonly DevOpsConfigurationAccessor _configurationAccessor; - - public sealed class Settings : CommandSettings - { - } - - public InitCommand(IAnsiConsole console, DevOpsConfigurationProtector protector, DevOpsConfigurationAccessor configurationAccessor) - { - _console = console; - _protector = protector; - _configurationAccessor = configurationAccessor; - } - - private void SaveConfiguration(DevOpsConfiguration config) - { - var saveConfig = new DevOpsConfiguration() - { - CollectionPAT = config.CollectionPAT, - CollectionUri = config.CollectionUri - }; - - _protector.Encrypt(saveConfig); - - var configAsJson = JsonSerializer.Serialize(config); - var configFile = "{\"DevOps\":" + configAsJson + "\n}"; - - if (!Directory.Exists(Constants.SettingsDirectory)) - { - Directory.CreateDirectory(Constants.SettingsDirectory); - } - - var path = Constants.SettingsPath; - File.WriteAllText(path, configFile); - - _console.WriteLine($"Updated encrypted devops settings at '${path}'."); - } - public override int Execute([NotNull] CommandContext context, [NotNull] Settings settings) { - - - _console.WriteLine("To connect to AzureDevops we'll need the url to your project collection and a PAT authorized to view it"); - _console.WriteLine("Examples:"); - _console.WriteLine("Url - https://mycompany.visualstudio.com/defaultcollection"); - _console.WriteLine("PAT - apatfromazuredevopswhichisalongstringofcharacters"); - _console.WriteLine(""); + console.WriteLine( + "To connect to AzureDevops we'll need the url to your project collection and a PAT authorized to view it"); + console.WriteLine("Examples:"); + console.WriteLine("Url - https://mycompany.visualstudio.com/defaultcollection"); + console.WriteLine("PAT - apatfromazuredevopswhichisalongstringofcharacters"); + console.WriteLine(""); - var uri = _console.Ask("Enter DevOps [green]Url[/] :"); + var uri = console.Ask("Enter DevOps [green]Url[/] :"); - var pat = _console.Ask("Enter DevOps [green]PAT[/] :"); + var pat = console.Ask("Enter DevOps [green]PAT[/] :"); var update = new DevOpsConfiguration { @@ -77,14 +30,19 @@ public override int Execute([NotNull] CommandContext context, [NotNull] Settings CollectionPAT = pat }; - _configurationAccessor.OverrideSettings(update); + // Save it to disk + configStore.SaveConfiguration(update, Constants.SettingsPath); - - SaveConfiguration(update); + // Update the loaded settings + configurationAccessor.UpdateSettings(update); - _console.WriteLine(""); + console.WriteLine($"Updated encrypted devops settings at '${Constants.SettingsPath}'."); + console.WriteLine(""); return 0; } + public sealed class Settings : CommandSettings + { + } } \ No newline at end of file diff --git a/src/Commands/ListCommand.cs b/src/Commands/ListCommand.cs index f1deb57..e5956a0 100644 --- a/src/Commands/ListCommand.cs +++ b/src/Commands/ListCommand.cs @@ -1,7 +1,6 @@ using System.Collections.Concurrent; using System.ComponentModel; using devops.Internal; -using Microsoft.Extensions.Options; using Microsoft.TeamFoundation.Build.WebApi; using Microsoft.TeamFoundation.Core.WebApi; using Spectre.Console; @@ -9,13 +8,9 @@ namespace devops.Commands; -public class ListCommand : AuthorizedCommandBase +public class ListCommand(IAnsiConsole console, DevOpsConfigurationAccessor devoptions) + : AuthorizedCommandBase(console, devoptions) { - public ListCommand(IAnsiConsole console, DevOpsConfigurationAccessor devoptions) : base(console, devoptions) - { - - } - public override async Task ExecuteAsync(CommandContext context, Settings settings) { var authResult = await base.ExecuteAsync(context, settings); @@ -32,14 +27,9 @@ public override async Task ExecuteAsync(CommandContext context, Settings se var data = new ConcurrentDictionary>(); - await _console.Progress() + await Console.Progress() .HideCompleted(false) - .Columns(new ProgressColumn[] - { - new TaskDescriptionColumn(), // Task description - new ProgressBarColumn(), // Progress bar - new SpinnerColumn() // Spinner - }) + .Columns(new TaskDescriptionColumn(), new ProgressBarColumn(), new SpinnerColumn()) .StartAsync(async ctx => { var task1 = ctx.AddTask("[green] Checking projects..[/]", true, projects.Count); @@ -91,11 +81,35 @@ await Parallel.ForEachAsync(projects, CancellationToken.None, async (project, to } // Render the table to the console - _console.Write(table); + Console.Write(table); return 0; } + private static string GetBuildResultEmoji(BuildResult? result) + { + if (result == null) + { + return "🏃"; + } + + switch (result.Value) + { + case BuildResult.None: + return "❔"; + case BuildResult.Succeeded: + return "✅"; //"✔"; + case BuildResult.PartiallySucceeded: + return "⚠"; + case BuildResult.Failed: + return "⛔"; + case BuildResult.Canceled: + return "🛑"; + } + + return "❔"; + } + private static List GetBuildRows(Settings settings, List builds) { var grouped = builds.GroupBy(b => new @@ -144,30 +158,6 @@ private static List GetBuildRows(Settings settings, List builds return rows; } - private static string GetBuildResultEmoji(BuildResult? result) - { - if (result == null) - { - return "🏃"; - } - - switch (result.Value) - { - case BuildResult.None: - return "❔"; - case BuildResult.Succeeded: - return "✅"; //"✔"; - case BuildResult.PartiallySucceeded: - return "⚠"; - case BuildResult.Failed: - return "⛔"; - case BuildResult.Canceled: - return "🛑"; - } - - return "❔"; - } - public class Settings : CommandSettings { [CommandOption("-f|--failed")] diff --git a/src/Commands/WatchCommand.cs b/src/Commands/WatchCommand.cs index ea58b9f..763f484 100644 --- a/src/Commands/WatchCommand.cs +++ b/src/Commands/WatchCommand.cs @@ -1,7 +1,6 @@ using System.Collections.Concurrent; using System.ComponentModel; using devops.Internal; -using Microsoft.Extensions.Options; using Microsoft.TeamFoundation.Build.WebApi; using Microsoft.TeamFoundation.Core.WebApi; using Microsoft.VisualStudio.Services.ReleaseManagement.WebApi; @@ -11,20 +10,15 @@ namespace devops.Commands; -public class WatchCommand : AuthorizedCommandBase +public class WatchCommand(IAnsiConsole console, DevOpsConfigurationAccessor devoptions) + : AuthorizedCommandBase(console, devoptions) { - private BuildHttpClient? _buildClient; private ProjectHttpClient? _projectClient; private ReleaseHttpClient2? _releaseClient; - public WatchCommand(IAnsiConsole console, DevOpsConfigurationAccessor devoptions) : base(console, devoptions) - { - - } - public override async Task ExecuteAsync(CommandContext context, Settings settings) { var authResult = await base.ExecuteAsync(context, settings); @@ -45,14 +39,9 @@ public override async Task ExecuteAsync(CommandContext context, Settings se var buildsByProject = new ConcurrentDictionary>(); var releasesByProject = new ConcurrentDictionary>(); - await _console.Progress() + await Console.Progress() .HideCompleted(false) - .Columns(new ProgressColumn[] - { - new TaskDescriptionColumn(), // Task description - new ProgressBarColumn(), // Progress bar - new SpinnerColumn() // Spinner - }) + .Columns(new TaskDescriptionColumn(), new ProgressBarColumn(), new SpinnerColumn()) .StartAsync(async ctx => { var task1 = ctx.AddTask("[green] Checking projects for active builds..[/]", true, projects.Count); @@ -78,10 +67,9 @@ await Parallel.ForEachAsync(projects, CancellationToken.None, async (project, to maxBuildsPerDefinition: 1, cancellationToken: token); - builds = builds.Where(d => d.Status == BuildStatus.InProgress || d.Status == BuildStatus.NotStarted) - .ToList(); + var activeBuilds = builds.Where(IsActiveOrPending).ToList(); - if (builds.Any()) + if (activeBuilds.Any()) { buildsByProject[project] = builds; } @@ -100,8 +88,9 @@ await Parallel.ForEachAsync(projects, CancellationToken.None, async (project, to if (settings.Continuous == true && buildsByProject.IsEmpty && releasesByProject.IsEmpty) { - Thread.Sleep(5000); task1.Value = 0; + + await Task.Delay(5000); } else { @@ -112,7 +101,7 @@ await Parallel.ForEachAsync(projects, CancellationToken.None, async (project, to if (buildsByProject.IsEmpty && releasesByProject.IsEmpty) { - _console.WriteLine("All builds & releases complete!"); + Console.WriteLine("All builds & releases complete!"); return 0; } @@ -149,7 +138,7 @@ await Parallel.ForEachAsync(projects, CancellationToken.None, async (project, to } } - await _console.Live(table) + await Console.Live(table) .StartAsync(async ctx => { while (true) @@ -171,19 +160,22 @@ await _console.Live(table) entry.Value.Status != BuildStatus.InProgress && entry.Value.Status != BuildStatus.NotStarted).ToArray(); + /* foreach (var f in finished) { buildRows.Remove(f.Key, out var _); - Console.Beep(); - } + }*/ - if (buildRows.IsEmpty) + var activeBuilds = buildRows.Any(entry => IsActiveOrPending(entry.Value) || + entry.Value.FinishTime > DateTime.UtcNow.AddMinutes(-1)); + + if (activeBuilds == false) { return 0; } - Thread.Sleep(1000); + await Task.Delay(1000); } }); @@ -192,30 +184,34 @@ await _console.Live(table) break; } - Thread.Sleep(5000); + await Task.Delay(5000); } return 0; } - private async Task UpdateBuildStatus(ICollection builds) + private static string GetBuildResultEmoji(BuildResult? result) { - if (_buildClient == null) + if (result == null) { - return; + return "🏃"; } - await Parallel.ForEachAsync(builds, CancellationToken.None, async (build, token) => + switch (result.Value) { - var updatedBuild = await _buildClient.GetBuildAsync( - build.Project.Id, build.Id, - - //propertyFilters: - cancellationToken: token); + case BuildResult.None: + return "❔"; + case BuildResult.Succeeded: + return "✅"; //"✔"; + case BuildResult.PartiallySucceeded: + return "⚠"; + case BuildResult.Failed: + return "⛔"; + case BuildResult.Canceled: + return "🛑"; + } - build.Status = updatedBuild.Status; - build.FinishTime = updatedBuild.FinishTime; - }); + return "❔"; } private static List GetBuildRow(Settings settings, Build build) @@ -226,40 +222,84 @@ private static List GetBuildRow(Settings settings, Build build) { GetBuildResultEmoji(build.Result) + " " + build.Definition.Name, build.BuildNumber, - build.Status?.ToString() ?? " ", + GetBuildStatusText(build.Status, build.Result), build.FinishTime?.ToString() ?? "" }); return row; } - private static string GetBuildResultEmoji(BuildResult? result) + private static string GetBuildStatusText(BuildStatus? status, BuildResult? result) { - if (result == null) + return result switch { - return "🏃"; + BuildResult.Failed => "Failed", + BuildResult.Canceled => "Cancelled", + _ => status?.ToString() ?? "" + }; + } + + protected bool IsActiveOrPending(Build build) => build.Status == BuildStatus.InProgress || + build.Status == BuildStatus.NotStarted && + build.QueueTime >= DateTime.UtcNow.AddMinutes(-10); + + private static void MakeSound(BuildResult? result) + { + if (!OperatingSystem.IsWindows()) + { + System.Console.Beep(); + return; } - switch (result.Value) + switch (result) { - case BuildResult.None: - return "❔"; case BuildResult.Succeeded: - return "✅"; //"✔"; + System.Console.Beep(800, 200); + break; case BuildResult.PartiallySucceeded: - return "⚠"; + System.Console.Beep(); + break; case BuildResult.Failed: - return "⛔"; + System.Console.Beep(240, 200); + break; case BuildResult.Canceled: - return "🛑"; + System.Console.Beep(300, 200); + break; } + } - return "❔"; + private async Task UpdateBuildStatus(ICollection builds) + { + if (_buildClient == null) + { + return; + } + + await Parallel.ForEachAsync(builds, CancellationToken.None, async (build, token) => + { + var updatedBuild = await _buildClient.GetBuildAsync( + build.Project.Id, build.Id, + + //propertyFilters: + cancellationToken: token); + + // Status Changed + if (build.Status == BuildStatus.InProgress && updatedBuild.Status != BuildStatus.InProgress) + { + MakeSound(updatedBuild.Result); + } + + build.BuildNumber = updatedBuild.BuildNumber; + build.Status = updatedBuild.Status; + build.Result = updatedBuild.Result; + build.FinishTime = updatedBuild.FinishTime; + }); } public class Settings : CommandSettings { - [CommandOption("-p|--project")] public string? Project { get; set; } + [CommandOption("-p|--project")] + public string? Project { get; set; } [CommandOption("-c|--continuous")] [DefaultValue(true)] diff --git a/src/DotnetDevops.csproj b/src/DotnetDevops.csproj index 21709d8..75df338 100644 --- a/src/DotnetDevops.csproj +++ b/src/DotnetDevops.csproj @@ -2,12 +2,12 @@ Exe - net6.0 + net8.0;net6.0 enable enable devops devops - 1.0.1 + 1.2.0 true devops James White @@ -40,28 +40,31 @@ - - True - \ - + + - - - - + + + + + + - - + + + + - - - - - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + @@ -72,4 +75,4 @@ - + \ No newline at end of file diff --git a/src/Internal/Constants.cs b/src/Internal/Constants.cs index 781e535..1cc804b 100644 --- a/src/Internal/Constants.cs +++ b/src/Internal/Constants.cs @@ -1,19 +1,19 @@ -using System.Reflection; - -namespace devops.Internal; +namespace devops.Internal; public static class Constants { - public static readonly int UnauthorizedExitCode = -1000; + public const string AppName = "dotnet-devops"; public const string SettingsAppName = "AE71EE95-49BD-40A9-81CD-B1DFD873EEA8"; + public const string SettingsFileName = "dotnet-devops.secrets.json"; public static readonly string UserProfileDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); public static readonly string SettingsDirectory = Path.Combine(UserProfileDirectory, ".dotnet-devops"); - + //Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; - public static readonly string SettingsPath = Path.Combine(SettingsDirectory, SettingsFileName); + public static readonly int UnauthorizedExitCode = -1000; + public static readonly string SettingsPath = Path.Combine(SettingsDirectory, SettingsFileName); } \ No newline at end of file diff --git a/src/Internal/DevOpsConfiguration.cs b/src/Internal/DevOpsConfiguration.cs index 2869e56..80f9b3e 100644 --- a/src/Internal/DevOpsConfiguration.cs +++ b/src/Internal/DevOpsConfiguration.cs @@ -1,10 +1,7 @@ -using System.ComponentModel.DataAnnotations; - -namespace devops.Internal; +namespace devops.Internal; public class DevOpsConfiguration { - // Be sure to use the full collection uri, i.e. http://myserver:8080/tfs/defaultcollection public string CollectionUri { get; set; } = string.Empty; diff --git a/src/Internal/DevOpsConfigurationAccessor.cs b/src/Internal/DevOpsConfigurationAccessor.cs index 669653b..5a01459 100644 --- a/src/Internal/DevOpsConfigurationAccessor.cs +++ b/src/Internal/DevOpsConfigurationAccessor.cs @@ -2,23 +2,14 @@ namespace devops.Internal; -public class DevOpsConfigurationAccessor +public class DevOpsConfigurationAccessor(IOptionsSnapshot settings) { - private readonly IOptionsSnapshot _settings; + private static DevOpsConfiguration? _overrideSettings; - private static DevOpsConfiguration? _overrideSettings = null; + public DevOpsConfiguration GetSettings() => _overrideSettings ?? settings.Value; - public DevOpsConfiguration GetSettings() => _overrideSettings ?? _settings.Value; - - public void OverrideSettings(DevOpsConfiguration settings) - { - _overrideSettings = settings; - } - - public DevOpsConfigurationAccessor(IOptionsSnapshot settings) + public void UpdateSettings(DevOpsConfiguration updatedSettings) { - _settings = settings; + _overrideSettings = updatedSettings; } - - } \ No newline at end of file diff --git a/src/Internal/DevOpsConfigurationDecryptor.cs b/src/Internal/DevOpsConfigurationDecryptor.cs index 406845d..efc2438 100644 --- a/src/Internal/DevOpsConfigurationDecryptor.cs +++ b/src/Internal/DevOpsConfigurationDecryptor.cs @@ -3,17 +3,10 @@ namespace devops.Internal; -public class DevOpsConfigurationProtector : +public class DevOpsConfigurationProtector(IDataProtectionProvider dataProtectionProvider) : IPostConfigureOptions { - private readonly IDataProtectionProvider _dataProtectionProvider; - - public DevOpsConfigurationProtector(IDataProtectionProvider dataProtectionProvider) - { - _dataProtectionProvider = dataProtectionProvider; - } - - public void PostConfigure(string name, DevOpsConfiguration options) + public void PostConfigure(string? name, DevOpsConfiguration options) { if (options.CollectionUri.StartsWith("http")) { @@ -31,14 +24,14 @@ public void PostConfigure(string name, DevOpsConfiguration options) private void Decrypt(DevOpsConfiguration config) { - var protector = _dataProtectionProvider.CreateProtector(Constants.SettingsAppName); + var protector = dataProtectionProvider.CreateProtector(Constants.SettingsAppName); config.CollectionUri = protector.Unprotect(config.CollectionUri); config.CollectionPAT = protector.Unprotect(config.CollectionPAT); } public void Encrypt(DevOpsConfiguration config) { - var protector = _dataProtectionProvider.CreateProtector(Constants.SettingsAppName); + var protector = dataProtectionProvider.CreateProtector(Constants.SettingsAppName); config.CollectionUri = protector.Protect(config.CollectionUri); config.CollectionPAT = protector.Protect(config.CollectionPAT); } diff --git a/src/Internal/DevOpsConfigurationStore.cs b/src/Internal/DevOpsConfigurationStore.cs new file mode 100644 index 0000000..94f8678 --- /dev/null +++ b/src/Internal/DevOpsConfigurationStore.cs @@ -0,0 +1,27 @@ +using System.Text.Json; + +namespace devops.Internal; + +public class DevOpsConfigurationStore(DevOpsConfigurationProtector protector) +{ + public void SaveConfiguration(DevOpsConfiguration config, string path) + { + var saveConfig = new DevOpsConfiguration + { + CollectionPAT = config.CollectionPAT, + CollectionUri = config.CollectionUri + }; + + protector.Encrypt(saveConfig); + + var configAsJson = JsonSerializer.Serialize(config); + var configFile = "{\"DevOps\":" + configAsJson + "\n}"; + + if (!Directory.Exists(Constants.SettingsDirectory)) + { + Directory.CreateDirectory(Constants.SettingsDirectory); + } + + File.WriteAllText(path, configFile); + } +} \ No newline at end of file diff --git a/src/Internal/DevOpsConfigurationValidation.cs b/src/Internal/DevOpsConfigurationValidation.cs index 508533c..cb41b7d 100644 --- a/src/Internal/DevOpsConfigurationValidation.cs +++ b/src/Internal/DevOpsConfigurationValidation.cs @@ -4,14 +4,17 @@ namespace devops.Internal; public class DevOpsConfigurationValidation : IValidateOptions { - public ValidateOptionsResult Validate(string name, DevOpsConfiguration options) + public ValidateOptionsResult Validate(string? name, DevOpsConfiguration options) { if (string.IsNullOrEmpty(options.CollectionUri)) + { return ValidateOptionsResult.Fail("CollectionUri must be set or updated using dotnet init"); + } if (string.IsNullOrEmpty(options.CollectionPAT)) + { return ValidateOptionsResult.Fail("CollectionPAT must be set or updated using dotnet init"); - + } return ValidateOptionsResult.Success; } diff --git a/src/Program.cs b/src/Program.cs index eb2c877..8f600db 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -1,98 +1,103 @@ using System.Diagnostics; -using System.Reflection; using System.Text; +using Community.Extensions.Spectre.Cli.Hosting; using devops.Commands; using devops.Internal; using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Internal; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Spectre.Cli.Extensions.DependencyInjection; +using Spectre.Console; using Spectre.Console.Cli; Console.OutputEncoding = Encoding.Default; -#region Configuration +Console.OutputEncoding = Encoding.Default; -var configuration = new ConfigurationBuilder() - .AddJsonFile(Constants.SettingsPath, true, true) - .AddCommandLine(args) - .AddEnvironmentVariables() - .Build(); +var builder = Host.CreateApplicationBuilder(args); +builder.Services.AddSingleton(); -#endregion +#region ⚙️ Configuration -#region Services +builder.Configuration.AddJsonFile(Constants.SettingsPath, true, true); -var services = new ServiceCollection(); -services.AddDataProtection().SetApplicationName(Constants.SettingsAppName).DisableAutomaticKeyGeneration(); -services.AddSingleton(configuration); -services.Configure(configuration.GetSection("DevOps")); -services.AddTransient(); -services.ConfigureOptions(); -services.AddTransient, DevOpsConfigurationValidation>(); -services.AddSingleton(); #endregion -#region Logging +#region 📰 Logging + +builder.Logging.AddSimpleConsole(opts => { opts.TimestampFormat = "yyyy-MM-dd HH:mm:ss "; }); -services.AddLogging(logging => +builder.Logging.AddFilter((cat, level) => { - logging.AddFilter((cat, level) => + if (cat?.StartsWith("Microsoft") == true) { - if (cat.StartsWith("Microsoft")) - { - return level > LogLevel.Information; - } + return level > LogLevel.Information; + } - return level > LogLevel.Trace; - }); - - logging.AddSimpleConsole(opts => { opts.TimestampFormat = "yyyy-MM-dd HH:mm:ss "; }); +#if DEBUG + return level > LogLevel.Debug; +#else + return level > LogLevel.Debug; +#endif }); #endregion -if (args.Length == 0) -{ - args = new[] { "watch" }; -} +#region 🎾 Services + +builder.Services.AddDataProtection() + .SetApplicationName(Constants.SettingsAppName) + .DisableAutomaticKeyGeneration(); -var registrar = new DependencyInjectionRegistrar(services); -var app = new CommandApp(registrar); +builder.Services.Configure(builder.Configuration.GetSection("DevOps")); +builder.Services.AddTransient(); +builder.Services.AddTransient(); +builder.Services.ConfigureOptions(); +builder.Services.AddTransient, DevOpsConfigurationValidation>(); +builder.Services.AddTransient(); -app.Configure(config => +#endregion + +#region 🐶 Commands + +builder.Services.AddCommand("init"); +builder.Services.AddCommand("list"); +builder.Services.AddCommand("watch"); + +builder.UseSpectreConsole(config => { #if DEBUG config.PropagateExceptions(); config.ValidateExamples(); #endif - config.AddCommand("init"); - config.AddCommand("list"); - config.AddCommand("watch"); + config.SetApplicationName(Constants.AppName); + config.UseBasicExceptionHandler(); }); +#endregion + +var app = builder.Build(); + run: -var exitCode = await app.RunAsync(args); +await app.RunAsync(); -if (exitCode == Constants.UnauthorizedExitCode) +if (Environment.ExitCode == Constants.UnauthorizedExitCode) { - var initCode = await app.RunAsync(new[] { "init" }); - + var cmdApp = app.Services.GetRequiredService(); + Environment.ExitCode = await cmdApp.RunAsync(new[] { "init" }); goto run; - - //Console.WriteLine("Please run 'devops' again to use the new DevOps settings."); } #if DEBUG if (Debugger.IsAttached) { - Console.WriteLine("Hit any key to exit"); - Console.ReadKey(); + AnsiConsole.Ask("Hit to exit"); } #endif -return exitCode; \ No newline at end of file +return Environment.ExitCode; \ No newline at end of file diff --git a/src/Properties/launchSettings.json b/src/Properties/launchSettings.json new file mode 100644 index 0000000..1a075e4 --- /dev/null +++ b/src/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "profiles": { + "DotnetDevops": { + "commandName": "Project", + "commandLineArgs": "watch" + }, + "Interactive": { + "commandName": "Executable", + "executablePath": "pwsh.exe", + "commandLineArgs": "-NoExit -c \"Set-Alias -Name devops -Value \"$(TargetDir)$(AssemblyName).exe\"", + "workingDirectory": "$(ProjectDir)" + } + } +} \ No newline at end of file