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
18 changes: 16 additions & 2 deletions .claude/commands/version.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,22 @@ Update ALL five version properties to the new value:

#### 2. `docs/man/man1/keystone-cli.1`

Update the VERSION section. Find the line that starts with `keystone-cli` under the
`.Sh VERSION` section and update it to `keystone-cli X.Y.Z`.
Update TWO sections in this file:

**a) The `.Dd` date tag (line 1):**

Run the locale-safe date script to get the current month and year:

```bash
./scripts/get-english-month-year.sh
```

Update the first line of the man page to `.Dd <output>` (e.g., `.Dd January 2026`).

**b) The VERSION section:**

Find the line that starts with `keystone-cli` under the `.Sh VERSION` section and
update it to `keystone-cli X.Y.Z`.

#### 3. `tests/Keystone.Cli.UnitTests/Application/Commands/Info/InfoCommandTests.cs`

Expand Down
29 changes: 29 additions & 0 deletions scripts/get-english-month-year.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -euo pipefail

# Returns the current month and year in English format: "Month YYYY"
# This script is locale-independent and works on both macOS (BSD) and Linux (GNU).

month_num=$(date '+%m')
year=$(date '+%Y')

case "$month_num" in
01) month_name="January" ;;
02) month_name="February" ;;
03) month_name="March" ;;
04) month_name="April" ;;
05) month_name="May" ;;
06) month_name="June" ;;
07) month_name="July" ;;
08) month_name="August" ;;
09) month_name="September" ;;
10) month_name="October" ;;
11) month_name="November" ;;
12) month_name="December" ;;
*)
echo "ERROR: Unexpected month number: $month_num" >&2
exit 1
;;
esac

echo "${month_name} ${year}"
21 changes: 21 additions & 0 deletions src/Keystone.Cli/Domain/CliInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace Keystone.Cli.Domain;

/// <summary>
/// Metadata about the Keystone CLI application itself.
/// </summary>
public static class CliInfo
{
/// <summary>
/// The year the Keystone CLI was created.
/// </summary>
public const int InceptionYear = 2025;

/// <summary>
/// The current year in local time.
/// </summary>
/// <remarks>
/// Uses local time to match the date script (<c>scripts/get-english-month-year.sh</c>),
/// which uses <c>date '+%Y'</c> (local time, not UTC).
/// </remarks>
public static int CurrentYear => DateTime.Now.Year;
}
75 changes: 75 additions & 0 deletions tests/Keystone.Cli.UnitTests/Docs/ManPageDateTagTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System.Text.RegularExpressions;
using Keystone.Cli.Domain;
using Keystone.Cli.UnitTests.TestUtilities;


namespace Keystone.Cli.UnitTests.Docs;

[TestFixture, Parallelizable(ParallelScope.All)]
public partial class ManPageDateTagTests
{
private static readonly string[] ValidEnglishMonths =
[
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];

[GeneratedRegex(@"^\.Dd ([A-Za-z]+) (\d{4})$")]
private static partial Regex DdTagPattern();

[Test]
public void ManPage_HasExactlyOneDdTag_WithValidEnglishMonthAndYear()
{
var manPagePath = GetManPagePath();
var lines = File.ReadAllLines(manPagePath);

var ddLines = lines
.Select((line, index) => (Line: line, LineNumber: index + 1))
.Where(x => x.Line.StartsWith(".Dd"))
.ToList();

Assert.That(ddLines, Has.Count.EqualTo(1), "Expected exactly one .Dd line in the man page");

var (ddLine, lineNumber) = ddLines[0];
var match = DdTagPattern().Match(ddLine);

Assert.That(
match.Success,
Is.True,
$"Line {lineNumber}: .Dd tag does not match expected pattern '.Dd Month YYYY'. Actual: '{ddLine}'"
);

var month = match.Groups[1].Value;
Assert.That(
ValidEnglishMonths,
Does.Contain(month),
$"Line {lineNumber}: Month '{month}' is not a valid English month name"
);

var year = int.Parse(match.Groups[2].Value);
Assert.That(
year,
Is.GreaterThanOrEqualTo(CliInfo.InceptionYear).And.LessThanOrEqualTo(CliInfo.CurrentYear),
$"Line {lineNumber}: Year {year} is outside reasonable range ({CliInfo.InceptionYear}-{CliInfo.CurrentYear})"
);
}

private static string GetManPagePath()
{
var manPagePath = Path.Combine(RepoPathResolver.GetRepoRoot(), "docs", "man", "man1", "keystone-cli.1");

return ! File.Exists(manPagePath)
? throw new FileNotFoundException($"Man page not found at expected path: {manPagePath}")
: manPagePath;
}
}
26 changes: 26 additions & 0 deletions tests/Keystone.Cli.UnitTests/TestUtilities/RepoPathResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace Keystone.Cli.UnitTests.TestUtilities;

/// <summary>
/// Resolves paths relative to the repository root for tests that need to access repo files.
/// </summary>
public static class RepoPathResolver
{
private const string SolutionFileName = "keystone-cli.sln";

/// <summary>
/// Finds the repository root by walking up from the test's base directory.
/// </summary>
/// <returns>The absolute path to the repository root.</returns>
/// <exception cref="InvalidOperationException">Thrown when the repository root cannot be found.</exception>
public static string GetRepoRoot()
=> FindRepoRoot(new DirectoryInfo(AppContext.BaseDirectory))
?? throw new InvalidOperationException("Could not find repository root. Ensure the test is running from within the repository.");

private static string? FindRepoRoot(DirectoryInfo? directory)
=> directory switch
{
null => null,
_ when File.Exists(Path.Combine(directory.FullName, SolutionFileName)) => directory.FullName,
_ => FindRepoRoot(directory.Parent),
};
}