diff --git a/.claude/commands/version.md b/.claude/commands/version.md index 07e8642..2c2dba8 100644 --- a/.claude/commands/version.md +++ b/.claude/commands/version.md @@ -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 ` (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` diff --git a/scripts/get-english-month-year.sh b/scripts/get-english-month-year.sh new file mode 100755 index 0000000..a7d4071 --- /dev/null +++ b/scripts/get-english-month-year.sh @@ -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}" diff --git a/src/Keystone.Cli/Domain/CliInfo.cs b/src/Keystone.Cli/Domain/CliInfo.cs new file mode 100644 index 0000000..ab75981 --- /dev/null +++ b/src/Keystone.Cli/Domain/CliInfo.cs @@ -0,0 +1,21 @@ +namespace Keystone.Cli.Domain; + +/// +/// Metadata about the Keystone CLI application itself. +/// +public static class CliInfo +{ + /// + /// The year the Keystone CLI was created. + /// + public const int InceptionYear = 2025; + + /// + /// The current year in local time. + /// + /// + /// Uses local time to match the date script (scripts/get-english-month-year.sh), + /// which uses date '+%Y' (local time, not UTC). + /// + public static int CurrentYear => DateTime.Now.Year; +} diff --git a/tests/Keystone.Cli.UnitTests/Docs/ManPageDateTagTests.cs b/tests/Keystone.Cli.UnitTests/Docs/ManPageDateTagTests.cs new file mode 100644 index 0000000..aaf5ff1 --- /dev/null +++ b/tests/Keystone.Cli.UnitTests/Docs/ManPageDateTagTests.cs @@ -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; + } +} diff --git a/tests/Keystone.Cli.UnitTests/TestUtilities/RepoPathResolver.cs b/tests/Keystone.Cli.UnitTests/TestUtilities/RepoPathResolver.cs new file mode 100644 index 0000000..268a2fb --- /dev/null +++ b/tests/Keystone.Cli.UnitTests/TestUtilities/RepoPathResolver.cs @@ -0,0 +1,26 @@ +namespace Keystone.Cli.UnitTests.TestUtilities; + +/// +/// Resolves paths relative to the repository root for tests that need to access repo files. +/// +public static class RepoPathResolver +{ + private const string SolutionFileName = "keystone-cli.sln"; + + /// + /// Finds the repository root by walking up from the test's base directory. + /// + /// The absolute path to the repository root. + /// Thrown when the repository root cannot be found. + 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), + }; +}