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");
+ }
}