Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 67 additions & 5 deletions CommandLine/AppVersionCli.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
namespace Wayfarer.CommandLine;

/// <summary>
/// Handles app-version CLI commands that do not require web host startup.
/// Handles app CLI commands that do not require web host startup.
/// </summary>
public static class AppVersionCli
{
/// <summary>
/// Handles the version command when present.
/// Handles supported app CLI commands when present.
/// </summary>
/// <param name="args">The process arguments.</param>
/// <param name="appVersionProvider">The provider for the compiled application version.</param>
Expand All @@ -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 <command> [options]

Commands:
version Print the compiled Wayfarer version.
reset-password <user> <pass> 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 <user> <pass>

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);
}
}
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
2 changes: 2 additions & 0 deletions docs/14-Setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <username> <new-password>`
- Use temporary values; rotate immediately. Do not document real passwords.

Expand Down
6 changes: 6 additions & 0 deletions docs/20-Deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
150 changes: 147 additions & 3 deletions tests/Wayfarer.Tests/Versioning/AppVersionCliTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,57 @@ namespace Wayfarer.Tests.Versioning;

public class AppVersionCliTests
{
private static readonly string TopLevelHelp = string.Join(
Environment.NewLine,
"Wayfarer CLI",
"",
"Usage:",
" Wayfarer <command> [options]",
"",
"Commands:",
" version Print the compiled Wayfarer version.",
" reset-password <user> <pass> 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 <user> <pass>",
"",
"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()
{
Expand All @@ -26,21 +77,109 @@ 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,
out var exitCode);

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
Expand All @@ -52,4 +191,9 @@ public StubAppVersionProvider(string version)

public string Version { get; }
}

private static string NormalizeLineEndings(string value)
{
return value.ReplaceLineEndings("\n");
}
}
Loading