diff --git a/CommandLine/AppVersionCli.cs b/CommandLine/AppVersionCli.cs index 918e9b8d..4822bc8d 100644 --- a/CommandLine/AppVersionCli.cs +++ b/CommandLine/AppVersionCli.cs @@ -3,12 +3,12 @@ namespace Wayfarer.CommandLine; /// -/// Handles app-version CLI commands that do not require web host startup. +/// Handles app CLI commands that do not require web host startup. /// public static class AppVersionCli { /// - /// Handles the version command when present. + /// Handles supported app CLI commands when present. /// /// The process arguments. /// The provider for the compiled application version. @@ -26,12 +26,74 @@ public static bool TryHandle( _ = error; exitCode = 0; - if (args.Length == 0 || !string.Equals(args[0], "version", StringComparison.OrdinalIgnoreCase)) + if (args.Length == 0) { return false; } - output.WriteLine($"Wayfarer {appVersionProvider.Version}"); - return true; + if (args.Length == 1 && (IsHelpCommand(args[0]) || IsHelpOption(args[0]))) + { + output.WriteLine(TopLevelHelp); + return true; + } + + if (string.Equals(args[0], "version", StringComparison.OrdinalIgnoreCase)) + { + if (args.Length == 2 && IsHelpOption(args[1])) + { + output.WriteLine(VersionHelp); + return true; + } + + output.WriteLine($"Wayfarer {appVersionProvider.Version}"); + return true; + } + + if (args.Length == 2 + && string.Equals(args[0], "reset-password", StringComparison.OrdinalIgnoreCase) + && IsHelpOption(args[1])) + { + output.WriteLine(ResetPasswordHelp); + return true; + } + + return false; + } + + private const string TopLevelHelp = """ + Wayfarer CLI + + Usage: + Wayfarer [options] + + Commands: + version Print the compiled Wayfarer version. + reset-password Reset a user's password. + help Show this help text. + """; + + private const string VersionHelp = """ + Usage: + Wayfarer version + + Prints the compiled Wayfarer version and exits before web host startup. + """; + + private const string ResetPasswordHelp = """ + Usage: + Wayfarer reset-password + + Resets a user's password using the configured application database. + """; + + private static bool IsHelpCommand(string value) + { + return string.Equals(value, "help", StringComparison.OrdinalIgnoreCase); + } + + private static bool IsHelpOption(string value) + { + return string.Equals(value, "--help", StringComparison.OrdinalIgnoreCase) + || string.Equals(value, "-h", StringComparison.OrdinalIgnoreCase); } } diff --git a/README.md b/README.md index 86da3338..ec5b2b68 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,15 @@ dotnet ef database update # apply Postgres/PostGIS migrations dotnet run # launch locally (reads appsettings.Development.json) ``` +Useful app CLI commands: + +```bash +dotnet run --no-launch-profile -- help +dotnet run --no-launch-profile -- version +``` + +For deployment/admin CLI details, including password reset, see the Deployment Guide. + For Vue Trip Editor development, run the ASP.NET Core app and the Vite dev server side-by-side: diff --git a/docs/14-Setup.md b/docs/14-Setup.md index bc1fdcab..9a1965c3 100644 --- a/docs/14-Setup.md +++ b/docs/14-Setup.md @@ -18,6 +18,8 @@ Database - The app auto‑creates Quartz tables at startup (`QuartzSchemaInstaller`). Admin CLI +- Help: `dotnet run --no-launch-profile -- help` +- Version: `dotnet run --no-launch-profile -- version` - Reset password: `dotnet run -- reset-password ` - Use temporary values; rotate immediately. Do not document real passwords. diff --git a/docs/20-Deployment.md b/docs/20-Deployment.md index de75e56a..d7ef203b 100644 --- a/docs/20-Deployment.md +++ b/docs/20-Deployment.md @@ -467,6 +467,12 @@ sudo tail -f /var/log/wayfarer/wayfarer-*.log ## CLI Commands +List app CLI commands: + +```bash +dotnet run --no-launch-profile -- help +``` + ### Password Reset Reset a user's password from the command line: diff --git a/tests/Wayfarer.Tests/Versioning/AppVersionCliTests.cs b/tests/Wayfarer.Tests/Versioning/AppVersionCliTests.cs index 0ee4fed8..006d9926 100644 --- a/tests/Wayfarer.Tests/Versioning/AppVersionCliTests.cs +++ b/tests/Wayfarer.Tests/Versioning/AppVersionCliTests.cs @@ -7,6 +7,57 @@ namespace Wayfarer.Tests.Versioning; public class AppVersionCliTests { + private static readonly string TopLevelHelp = string.Join( + Environment.NewLine, + "Wayfarer CLI", + "", + "Usage:", + " Wayfarer [options]", + "", + "Commands:", + " version Print the compiled Wayfarer version.", + " reset-password Reset a user's password.", + " help Show this help text.", + ""); + + private static readonly string VersionHelp = string.Join( + Environment.NewLine, + "Usage:", + " Wayfarer version", + "", + "Prints the compiled Wayfarer version and exits before web host startup.", + ""); + + private static readonly string ResetPasswordHelp = string.Join( + Environment.NewLine, + "Usage:", + " Wayfarer reset-password ", + "", + "Resets a user's password using the configured application database.", + ""); + + [Theory] + [InlineData("help")] + [InlineData("--help")] + [InlineData("-h")] + public void TryHandle_TopLevelHelpCommands_WriteExactHelpAndExitZero(string command) + { + using var output = new StringWriter(); + using var error = new StringWriter(); + + var handled = AppVersionCli.TryHandle( + new[] { command }, + new StubAppVersionProvider("1.4.0"), + output, + error, + out var exitCode); + + handled.Should().BeTrue(); + exitCode.Should().Be(0); + NormalizeLineEndings(output.ToString()).Should().Be(NormalizeLineEndings(TopLevelHelp)); + error.ToString().Should().BeEmpty(); + } + [Fact] public void TryHandle_VersionCommand_WritesExactVersionLine() { @@ -26,14 +77,37 @@ public void TryHandle_VersionCommand_WritesExactVersionLine() error.ToString().Should().BeEmpty(); } - [Fact] - public void TryHandle_VersionCommand_DoesNotRequireWebHostOrDatabaseStartup() + [Theory] + [InlineData("--help")] + [InlineData("-h")] + public void TryHandle_VersionHelpCommands_WriteExactHelpAndExitZero(string option) { using var output = new StringWriter(); using var error = new StringWriter(); var handled = AppVersionCli.TryHandle( - new[] { "version" }, + new[] { "version", option }, + new StubAppVersionProvider("1.4.0"), + output, + error, + out var exitCode); + + handled.Should().BeTrue(); + exitCode.Should().Be(0); + NormalizeLineEndings(output.ToString()).Should().Be(NormalizeLineEndings(VersionHelp)); + error.ToString().Should().BeEmpty(); + } + + [Theory] + [InlineData("--help")] + [InlineData("-h")] + public void TryHandle_ResetPasswordHelpCommands_WriteExactHelpAndExitZero(string option) + { + using var output = new StringWriter(); + using var error = new StringWriter(); + + var handled = AppVersionCli.TryHandle( + new[] { "reset-password", option }, new StubAppVersionProvider("1.4.0"), output, error, @@ -41,6 +115,71 @@ public void TryHandle_VersionCommand_DoesNotRequireWebHostOrDatabaseStartup() handled.Should().BeTrue(); exitCode.Should().Be(0); + NormalizeLineEndings(output.ToString()).Should().Be(NormalizeLineEndings(ResetPasswordHelp)); + error.ToString().Should().BeEmpty(); + } + + [Theory] + [InlineData("help")] + [InlineData("--help")] + [InlineData("-h")] + [InlineData("version")] + [InlineData("version", "--help")] + [InlineData("version", "-h")] + [InlineData("reset-password", "--help")] + [InlineData("reset-password", "-h")] + public void TryHandle_HostFreeCommands_AreHandledByPureCliSeam(params string[] args) + { + using var output = new StringWriter(); + using var error = new StringWriter(); + + var handled = AppVersionCli.TryHandle( + args, + new StubAppVersionProvider("1.4.0"), + output, + error, + out var exitCode); + + handled.Should().BeTrue(); + exitCode.Should().Be(0); + } + + [Fact] + public void TryHandle_UnknownCommand_KeepsCurrentFallThroughBehavior() + { + using var output = new StringWriter(); + using var error = new StringWriter(); + + var handled = AppVersionCli.TryHandle( + new[] { "unknown" }, + new StubAppVersionProvider("1.4.0"), + output, + error, + out var exitCode); + + handled.Should().BeFalse(); + exitCode.Should().Be(0); + output.ToString().Should().BeEmpty(); + error.ToString().Should().BeEmpty(); + } + + [Fact] + public void TryHandle_NormalResetPasswordCommand_RemainsUnhandled() + { + using var output = new StringWriter(); + using var error = new StringWriter(); + + var handled = AppVersionCli.TryHandle( + new[] { "reset-password", "user", "pass" }, + new StubAppVersionProvider("1.4.0"), + output, + error, + out var exitCode); + + handled.Should().BeFalse(); + exitCode.Should().Be(0); + output.ToString().Should().BeEmpty(); + error.ToString().Should().BeEmpty(); } private sealed class StubAppVersionProvider : IAppVersionProvider @@ -52,4 +191,9 @@ public StubAppVersionProvider(string version) public string Version { get; } } + + private static string NormalizeLineEndings(string value) + { + return value.ReplaceLineEndings("\n"); + } }