From a0de444f36495ddbba2aee640d404bc04737ac76 Mon Sep 17 00:00:00 2001 From: Chrison Simtian Date: Wed, 17 Jun 2026 22:04:06 +1200 Subject: [PATCH 1/2] Convert :secrets to SecretsCommand Lift the :secrets handler (plus its LoadSecrets/SaveSecrets helpers) into a standalone IFalloutCommand type taking IConsolePrompts via the constructor. Self-contained (no shared helpers). Replaces the DelegateCommand adapter; deletes Program.Secrets.cs. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../SecretsCommand.cs} | 26 +++++++++++++------ src/Fallout.Cli/Program.cs | 2 +- 2 files changed, 19 insertions(+), 9 deletions(-) rename src/Fallout.Cli/{Program.Secrets.cs => Commands/SecretsCommand.cs} (84%) diff --git a/src/Fallout.Cli/Program.Secrets.cs b/src/Fallout.Cli/Commands/SecretsCommand.cs similarity index 84% rename from src/Fallout.Cli/Program.Secrets.cs rename to src/Fallout.Cli/Commands/SecretsCommand.cs index d6dfc277a..6f718fcf7 100644 --- a/src/Fallout.Cli/Program.Secrets.cs +++ b/src/Fallout.Cli/Commands/SecretsCommand.cs @@ -1,7 +1,6 @@ -using System; using System.Collections.Generic; using System.Linq; -using System.Text.Json.Nodes; +using Fallout.Cli.Prompts; using Fallout.Common; using Fallout.Common.IO; using Fallout.Common.Utilities; @@ -9,20 +8,31 @@ using static Fallout.Common.Constants; using static Fallout.Common.Utilities.EncryptionUtility; -namespace Fallout.Cli; +namespace Fallout.Cli.Commands; // TODO: unlock prompt // TODO: environment variable name // TODO: profile vs. environment // TODO: nuke :profile -partial class Program + +/// +/// fallout :secrets: interactively encrypts secret parameters into the (optionally profile-scoped) +/// parameters file, managing the keychain password. +/// +public sealed class SecretsCommand : IFalloutCommand { private const string SaveAndExit = ""; private const string DiscardAndExit = ""; private const string DeletePasswordAndExit = ""; + private readonly IConsolePrompts _prompts; + + public SecretsCommand(IConsolePrompts prompts) => _prompts = prompts; + + public string Name => "secrets"; + // ReSharper disable once CognitiveComplexity - public static int Secrets(string[] args, AbsolutePath rootDirectory, AbsolutePath buildScript) + public int Execute(string[] args, AbsolutePath rootDirectory, AbsolutePath buildScript) { var secretParameters = CompletionUtility.GetItemsFromSchema( GetBuildSchemaFile(rootDirectory.NotNull("No root directory")), @@ -62,7 +72,7 @@ public static int Secrets(string[] args, AbsolutePath rootDirectory, AbsolutePat if (EnvironmentInfo.IsOsx && existingSecrets.Count == 0 && !fromCredentialStore) { - if (generatedPassword || PromptForConfirmation($"Save password to keychain? (associated with '{rootDirectory}')")) + if (generatedPassword || _prompts.PromptForConfirmation($"Save password to keychain? (associated with '{rootDirectory}')")) CredentialStore.SavePassword(credentialStoreName, password); } else if (fromLegacyCredentialStore) @@ -78,13 +88,13 @@ public static int Secrets(string[] args, AbsolutePath rootDirectory, AbsolutePat var addedSecrets = new Dictionary(); while (true) { - var choice = PromptForChoice( + var choice = _prompts.PromptForChoice( "Choose secret parameter to enter value:", options.Select(x => (x, addedSecrets.ContainsKey(x) || existingSecrets.ContainsKey(x) ? $"* {x}" : x)).ToArray()); if (!choice.EqualsAnyOrdinalIgnoreCase(SaveAndExit, DiscardAndExit, DeletePasswordAndExit)) { - addedSecrets[choice] = PromptForSecret(choice); + addedSecrets[choice] = _prompts.PromptForSecret(choice); } else { diff --git a/src/Fallout.Cli/Program.cs b/src/Fallout.Cli/Program.cs index 155e4603e..933f78434 100644 --- a/src/Fallout.Cli/Program.cs +++ b/src/Fallout.Cli/Program.cs @@ -62,12 +62,12 @@ private static void RegisterCommands(IServiceCollection services) // Legacy handlers still living on Program, adapted until they are extracted into command // types. Each conversion deletes one line here plus its Program.X.cs partial. services.AddSingleton(); + services.AddSingleton(); // Legacy handlers still living on Program, adapted until they are extracted into command // types. Each conversion deletes one line here plus its Program.X.cs partial. services.AddSingleton(new DelegateCommand("cake-convert", CakeConvert)); services.AddSingleton(new DelegateCommand("cake-clean", CakeClean)); - services.AddSingleton(new DelegateCommand("secrets", Secrets)); services.AddSingleton(new DelegateCommand("GetNextDirectory", GetNextDirectory)); services.AddSingleton(new DelegateCommand("PopDirectory", PopDirectory)); services.AddSingleton(new DelegateCommand("PushWithCurrentRootDirectory", PushWithCurrentRootDirectory)); From 504273ce4adb6fd56ddb2a002d367edab804b6c2 Mon Sep 17 00:00:00 2001 From: Chrison Simtian Date: Wed, 17 Jun 2026 22:33:46 +1200 Subject: [PATCH 2/2] Add tests for SecretsCommand Name resolution and the no-root-directory guard. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Commands/SecretsCommandTests.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 tests/Fallout.Cli.Tests/Commands/SecretsCommandTests.cs diff --git a/tests/Fallout.Cli.Tests/Commands/SecretsCommandTests.cs b/tests/Fallout.Cli.Tests/Commands/SecretsCommandTests.cs new file mode 100644 index 000000000..892f3a57f --- /dev/null +++ b/tests/Fallout.Cli.Tests/Commands/SecretsCommandTests.cs @@ -0,0 +1,22 @@ +using System; +using Fallout.Cli.Commands; +using FluentAssertions; +using Xunit; + +namespace Fallout.Cli.Tests.Commands; + +public class SecretsCommandTests +{ + [Fact] + public void Name_IsSecrets() + => new SecretsCommand(new FakeConsolePrompts()).Name.Should().Be("secrets"); + + [Fact] + public void Execute_WithoutRootDirectory_Throws() + { + var action = () => new SecretsCommand(new FakeConsolePrompts()) + .Execute([], rootDirectory: null, buildScript: null); + + action.Should().Throw().WithMessage("*No root directory*"); + } +}