diff --git a/.githooks/post-commit b/.githooks/post-commit new file mode 120000 index 0000000..50e32c5 --- /dev/null +++ b/.githooks/post-commit @@ -0,0 +1 @@ +post-commit.cs \ No newline at end of file diff --git a/.githooks/post-commit.cs b/.githooks/post-commit.cs new file mode 100755 index 0000000..397c955 --- /dev/null +++ b/.githooks/post-commit.cs @@ -0,0 +1,18 @@ +#!/usr/bin/env -S dotnet -- +#:package TimeWarp.Amuru +#:property NoWarn=CA2007 + +using TimeWarp.Amuru; + +string? root = Git.FindRoot(); +if (root is null) +{ + return 0; +} + +await Shell.Builder("ganda") + .WithArguments("memsearch", "index-repo", "--background") + .WithWorkingDirectory(root) + .WithNoValidation() + .RunAsync(); +return 0; \ No newline at end of file diff --git a/.githooks/post-merge b/.githooks/post-merge new file mode 120000 index 0000000..6031064 --- /dev/null +++ b/.githooks/post-merge @@ -0,0 +1 @@ +post-merge.cs \ No newline at end of file diff --git a/.githooks/post-merge.cs b/.githooks/post-merge.cs new file mode 100755 index 0000000..397c955 --- /dev/null +++ b/.githooks/post-merge.cs @@ -0,0 +1,18 @@ +#!/usr/bin/env -S dotnet -- +#:package TimeWarp.Amuru +#:property NoWarn=CA2007 + +using TimeWarp.Amuru; + +string? root = Git.FindRoot(); +if (root is null) +{ + return 0; +} + +await Shell.Builder("ganda") + .WithArguments("memsearch", "index-repo", "--background") + .WithWorkingDirectory(root) + .WithNoValidation() + .RunAsync(); +return 0; \ No newline at end of file diff --git a/.gitignore b/.gitignore index ce89292..48a1020 100644 --- a/.gitignore +++ b/.gitignore @@ -416,3 +416,6 @@ FodyWeavers.xsd *.msix *.msm *.msp + +# MemSearch local index data (machine-specific; .memsearch.toml is tracked) +.memsearch/ diff --git a/.memsearch.toml b/.memsearch.toml new file mode 100644 index 0000000..00bcc0e --- /dev/null +++ b/.memsearch.toml @@ -0,0 +1,50 @@ +# MemSearch repo knowledge index — TimeWarp convention +# +# SPEC (proposed extension) +# ------------------------- +# memsearch's stock CLI ignores [index]; this file is honored by: +# ganda memsearch index-repo +# ganda hooks install memsearch (post-commit / post-merge runfiles) +# +# Purpose: index markdown knowledge in THIS repo only. One Milvus collection +# per repo (auto-derived from repo path). Global DB file stays at +# ~/.memsearch/milvus.db unless [milvus].uri is overridden below. +# +# [index].paths +# Directory roots (relative to repo root). memsearch indexes all .md files +# under each path recursively. Missing paths are skipped. +# +# [index].exclude_paths +# Directory paths removed from the resolved path list before indexing. +# Does not prune subdirectories when a parent path (e.g. "kanban/") is listed. +# +# [index].exclude_globs +# Reserved — file-level globs (e.g. "**/task-template.md"). Not implemented in v1. +# +# [index].enabled +# false disables hook indexing (conversation plugins still work). +# +# [milvus].collection +# Empty = auto-derive ms__<8char_sha256> from repo root. +# +# Search this repo only: +# memsearch search "query" -c --source-prefix "$(pwd)/kanban/" + +[index] +enabled = true +paths = [ + "kanban", + "documentation", + "okf", + "adr", + ".memsearch/memory", +] +exclude_paths = [] +exclude_globs = [ + "**/task-template.md", + "**/overview.md", +] + +[milvus] +# uri = ".memsearch/milvus.db" # optional per-repo DB file +collection = "" \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index e76fb83..af00aed 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -19,5 +19,6 @@ "titleBar.activeForeground": "#e7e7e7", "titleBar.inactiveBackground": "#f61d1f99", "titleBar.inactiveForeground": "#e7e7e799" - } -} \ No newline at end of file + }, + "window.title": "timewarp-builder · ${rootName}${separator}${activeEditorShort}" +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..f82f57f --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,18 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "ganda: window icon", + "type": "shell", + "command": "ganda repo avatar window", + "runOptions": { + "runOn": "folderOpen" + }, + "presentation": { + "reveal": "silent", + "echo": false + }, + "problemMatcher": [] + } + ] +} diff --git a/BannedSymbols.txt b/BannedSymbols.txt index 167da45..7d8a827 100644 --- a/BannedSymbols.txt +++ b/BannedSymbols.txt @@ -1 +1,2 @@ -T:System.Console;Use TimeWarp.Terminal.Terminal static class or inject ITerminal instead +T:System.Console;Prefer injecting ITerminal. TimeWarp.Terminal.Terminal static class is available for migration. +T:System.Diagnostics.ProcessStartInfo;Use TimeWarp.Amuru Shell.Builder instead. See the 'amuru' skill for usage patterns. diff --git a/Directory.Build.props b/Directory.Build.props index e1d0149..9e072ba 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,3 +1,4 @@ + @@ -19,6 +20,9 @@ enable latest false + + + true @@ -70,4 +74,9 @@ - \ No newline at end of file + + + + + + diff --git a/Directory.Packages.props b/Directory.Packages.props index 49dcafd..9aed975 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,18 +6,10 @@ - - + - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - @@ -25,8 +17,4 @@ - - - - - \ No newline at end of file + diff --git a/documentation/.gitkeep b/documentation/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/kanban/archived/.gitkeep b/kanban/archived/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/kanban/backlog/.gitkeep b/kanban/backlog/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/kanban/to-do/.gitkeep b/kanban/to-do/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/skills/.gitkeep b/skills/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/source/Directory.Build.props b/source/Directory.Build.props index 85fb734..35cb2bd 100644 --- a/source/Directory.Build.props +++ b/source/Directory.Build.props @@ -4,7 +4,7 @@ - 1.0.0-beta.2 + 1.0.0-beta.3 Steven T. Cramer https://github.com/TimeWarpEngineering/timewarp-builder Unlicense @@ -29,8 +29,4 @@ - - - - diff --git a/tests/.gitkeep b/tests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/timewarp-builder.slnx b/timewarp-builder.slnx index 9c18b9c..9ae75ec 100644 --- a/timewarp-builder.slnx +++ b/timewarp-builder.slnx @@ -3,6 +3,6 @@ - + diff --git a/tools/dev-cli/Directory.Build.props b/tools/dev-cli/Directory.Build.props index 57c3b15..3474f1f 100644 --- a/tools/dev-cli/Directory.Build.props +++ b/tools/dev-cli/Directory.Build.props @@ -13,7 +13,9 @@ - $(NoWarn);CA1031;CA1303;CA1508;CA1515;CA1849;CA2007;CA2016;CA2000;RCS1046 + + + $(NoWarn);CA1031;CA1303;CA1508;CA1515;CA1849;CA2007;CA2016;CA2000;RCS1046;IDE0211;CS1591 true diff --git a/tools/dev-cli/dev.cs b/tools/dev-cli/dev.cs index b045f27..871afb2 100755 --- a/tools/dev-cli/dev.cs +++ b/tools/dev-cli/dev.cs @@ -1,4 +1,4 @@ -#!/usr/bin/dotnet -- +#!/usr/bin/env -S dotnet -- // ═══════════════════════════════════════════════════════════════════════════════ // DEV CLI - TIMEWARP.BUILDER DEVELOPMENT TOOL // ═══════════════════════════════════════════════════════════════════════════════ @@ -12,12 +12,15 @@ // As AOT: ./bin/dev // // Commands: -// dev build - Build the TimeWarp.Builder library -// dev test - Run tests -// dev clean - Clean solution and artifacts -// dev pack - Create NuGet packages -// dev format - Check/fix code formatting -// dev self-install - AOT compile and install dev CLI to ./bin +// dev build - Build the TimeWarp.Builder library +// dev test - Run tests +// dev clean - Clean solution and artifacts +// dev pack - Create NuGet packages +// dev format - Check/fix code formatting +// dev check-version - Verify version is ready to release +// dev verify-samples - Verify code samples compile +// dev workflow - Run full CI/CD pipeline +// dev self-install - AOT compile and install dev CLI to ./bin // // To bootstrap: // dotnet run tools/dev-cli/dev.cs -- self-install @@ -25,9 +28,13 @@ // dev --help // ═══════════════════════════════════════════════════════════════════════════════ +#region Purpose +// Runfile entry point for the dev CLI: builds the Nuru app and dispatches to the endpoint commands. +#endregion + NuruApp app = NuruApp.CreateBuilder() .WithName("dev") - .WithDescription("Development CLI for TimeWarp.Builder") + .WithDescription("Development CLI for timewarp-builder") .DiscoverEndpoints() .Build(); diff --git a/tools/dev-cli/endpoints/GlobalSuppressions.cs b/tools/dev-cli/endpoints/GlobalSuppressions.cs deleted file mode 100644 index e3c6ba3..0000000 --- a/tools/dev-cli/endpoints/GlobalSuppressions.cs +++ /dev/null @@ -1,8 +0,0 @@ -// This file is used by Code Analysis to maintain SuppressMessage -// attributes that are applied to this project. -// Project-level suppressions either have no target or are given -// a specific target and scoped to a namespace, type, member, etc. - -using System.Diagnostics.CodeAnalysis; - -[assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DevCli.Commands.BuildCommand.Handler.#ctor(ITerminal)")] diff --git a/tools/dev-cli/endpoints/build.cs b/tools/dev-cli/endpoints/build.cs index 3418724..6224492 100644 --- a/tools/dev-cli/endpoints/build.cs +++ b/tools/dev-cli/endpoints/build.cs @@ -3,6 +3,10 @@ // ═══════════════════════════════════════════════════════════════════════════════ // Builds the TimeWarp.Builder library. +#region Purpose +// 'dev build' endpoint: compiles source/timewarp-builder via dotnet build. +#endregion + namespace DevCli.Commands; /// diff --git a/tools/dev-cli/endpoints/check-version.cs b/tools/dev-cli/endpoints/check-version.cs new file mode 100644 index 0000000..7e8ab5c --- /dev/null +++ b/tools/dev-cli/endpoints/check-version.cs @@ -0,0 +1,67 @@ +// ═══════════════════════════════════════════════════════════════════════════════ +// CHECK-VERSION COMMAND +// ═══════════════════════════════════════════════════════════════════════════════ +// Verifies the source version is not already published on NuGet.org. + +#region Purpose +// 'dev check-version' endpoint: verifies the version in source/Directory.Build.props is not already published on NuGet.org. +#endregion + +namespace DevCli.Commands; + +using System.Xml.Linq; + +/// +/// Verify the version is ready to release. +/// +[NuruRoute("check-version", Description = "Verify version is ready to release")] +internal sealed class CheckVersionCommand : ICommand +{ + internal sealed class Handler : ICommandHandler + { + private readonly ITerminal Terminal; + + public Handler(ITerminal terminal) + { + Terminal = terminal; + } + + public async ValueTask Handle(CheckVersionCommand command, CancellationToken ct) + { + string? repoRoot = Git.FindRoot() ?? + throw new InvalidOperationException("Could not find git repository root (.git not found)"); + + string propsPath = Path.Combine(repoRoot, "source", "Directory.Build.props"); + + if (!File.Exists(propsPath)) + { + throw new FileNotFoundException($"Could not find {propsPath}"); + } + + XDocument doc = XDocument.Load(propsPath); + string? version = doc.Descendants("Version").FirstOrDefault()?.Value; + + if (string.IsNullOrEmpty(version)) + { + throw new InvalidOperationException("Could not find version in source/Directory.Build.props"); + } + + Terminal.WriteLine($"Checking if TimeWarp.Builder {version} is already published on NuGet.org..."); + + CommandOutput result = await Shell.Builder("dotnet") + .WithArguments("package", "search", "TimeWarp.Builder", "--exact-match", "--prerelease", "--source", "https://api.nuget.org/v3/index.json") + .WithNoValidation() + .CaptureAsync(); + + if (result.Stdout.Contains($"| {version} |", StringComparison.Ordinal)) + { + Terminal.WriteErrorLine($"TimeWarp.Builder {version} is already published. Increment the version in source/Directory.Build.props."); + Environment.ExitCode = 1; + return Value; + } + + Terminal.WriteLine($"✅ TimeWarp.Builder {version} is not yet published. Ready to release."); + return Value; + } + } +} diff --git a/tools/dev-cli/endpoints/clean.cs b/tools/dev-cli/endpoints/clean.cs index c58f766..0c5ac89 100644 --- a/tools/dev-cli/endpoints/clean.cs +++ b/tools/dev-cli/endpoints/clean.cs @@ -3,6 +3,10 @@ // ═══════════════════════════════════════════════════════════════════════════════ // Cleans the TimeWarp.Builder project and deletes all bin/obj directories. +#region Purpose +// 'dev clean' endpoint: runs dotnet clean and deletes bin/obj directories (sparing the dev CLI's own). +#endregion + namespace DevCli.Commands; /// @@ -48,12 +52,13 @@ public async ValueTask Handle(CleanCommand command, CancellationToken ct) } // Also delete obj and bin directories for a thorough clean + // (sparing the dev CLI's own directories and the repo-root bin that holds the installed dev binary) Terminal.WriteLine("\nDeleting obj and bin directories..."); string[] directoriesToDelete = [ .. Directory.GetDirectories(repoRoot, "obj", SearchOption.AllDirectories) .Concat(Directory.GetDirectories(repoRoot, "bin", SearchOption.AllDirectories)) - .Where(d => !d.Contains(Path.Combine("tools", "dev-cli"))), + .Where(d => !d.Contains(Path.Combine("tools", "dev-cli")) && d != Path.Combine(repoRoot, "bin")), ]; foreach (string dir in directoriesToDelete) diff --git a/tools/dev-cli/endpoints/format.cs b/tools/dev-cli/endpoints/format.cs index 32c95b1..908f87c 100644 --- a/tools/dev-cli/endpoints/format.cs +++ b/tools/dev-cli/endpoints/format.cs @@ -3,6 +3,10 @@ // ═══════════════════════════════════════════════════════════════════════════════ // Check or fix code formatting using dotnet format. +#region Purpose +// 'dev format' endpoint: checks or fixes code style via dotnet format. +#endregion + namespace DevCli.Commands; /// diff --git a/tools/dev-cli/endpoints/pack.cs b/tools/dev-cli/endpoints/pack.cs index 99ac37d..6dbddea 100644 --- a/tools/dev-cli/endpoints/pack.cs +++ b/tools/dev-cli/endpoints/pack.cs @@ -3,6 +3,10 @@ // ═══════════════════════════════════════════════════════════════════════════════ // Creates NuGet packages for the TimeWarp.Builder library. +#region Purpose +// 'dev pack' endpoint: packs TimeWarp.Builder into artifacts/packages. +#endregion + namespace DevCli.Commands; /// diff --git a/tools/dev-cli/endpoints/self-install.cs b/tools/dev-cli/endpoints/self-install.cs index 0d44b65..d709bad 100644 --- a/tools/dev-cli/endpoints/self-install.cs +++ b/tools/dev-cli/endpoints/self-install.cs @@ -3,6 +3,10 @@ // ═══════════════════════════════════════════════════════════════════════════════ // AOT compiles and installs the dev CLI to ./bin for fast execution via direnv PATH. +#region Purpose +// 'dev self-install' endpoint: AOT-publishes dev.cs to bin/dev for direnv PATH use. +#endregion + namespace DevCli.Commands; /// diff --git a/tools/dev-cli/endpoints/test.cs b/tools/dev-cli/endpoints/test.cs index 3fbc927..a5dda08 100644 --- a/tools/dev-cli/endpoints/test.cs +++ b/tools/dev-cli/endpoints/test.cs @@ -3,6 +3,10 @@ // ═══════════════════════════════════════════════════════════════════════════════ // Runs the test suite for TimeWarp.Builder. +#region Purpose +// 'dev test' endpoint: runs the solution test suite, skipping gracefully when tests/ is absent. +#endregion + namespace DevCli.Commands; /// @@ -39,9 +43,9 @@ public async ValueTask Handle(TestCommand command, CancellationToken ct) string testsDirectory = Path.Combine(repoRoot, "tests"); - if (!Directory.Exists(testsDirectory)) + if (!Directory.Exists(testsDirectory) || !Directory.EnumerateFiles(testsDirectory, "*.cs", SearchOption.AllDirectories).Any()) { - Terminal.WriteLine("No tests directory found. Skipping test step."); + Terminal.WriteLine("No tests found. Skipping test step."); return Value; } diff --git a/tools/dev-cli/endpoints/verify-samples.cs b/tools/dev-cli/endpoints/verify-samples.cs new file mode 100644 index 0000000..5620703 --- /dev/null +++ b/tools/dev-cli/endpoints/verify-samples.cs @@ -0,0 +1,85 @@ +// ═══════════════════════════════════════════════════════════════════════════════ +// VERIFY-SAMPLES COMMAND +// ═══════════════════════════════════════════════════════════════════════════════ +// Verifies that all code samples in the repository compile. + +#region Purpose +// 'dev verify-samples' endpoint: builds each sample to verify it compiles, skipping gracefully when samples/ is absent. +#endregion + +namespace DevCli.Commands; + +/// +/// Verify code samples compile. +/// +[NuruRoute("verify-samples", Description = "Verify code samples compile")] +internal sealed class VerifySamplesCommand : ICommand +{ + internal sealed class Handler : ICommandHandler + { + private readonly ITerminal Terminal; + + public Handler(ITerminal terminal) + { + Terminal = terminal; + } + + public async ValueTask Handle(VerifySamplesCommand command, CancellationToken ct) + { + string? repoRoot = Git.FindRoot() ?? + throw new InvalidOperationException("Could not find git repository root (.git not found)"); + + string samplesDirectory = Path.Combine(repoRoot, "samples"); + + if (!Directory.Exists(samplesDirectory)) + { + Terminal.WriteLine("No samples directory found. Skipping sample verification."); + return Value; + } + + string[] sampleFiles = Directory.GetFiles(samplesDirectory, "*.cs", SearchOption.AllDirectories); + + if (sampleFiles.Length == 0) + { + Terminal.WriteLine("No sample files found. Skipping sample verification."); + return Value; + } + + Terminal.WriteLine($"Verifying {sampleFiles.Length} sample file(s)..."); + + int failedCount = 0; + + foreach (string sampleFile in sampleFiles) + { + string relativePath = Path.GetRelativePath(repoRoot, sampleFile); + Terminal.WriteLine($" Compiling: {relativePath}"); + + int exitCode = await DotNet.Build() + .WithProject(sampleFile) + .WithConfiguration("Release") + .WithVerbosity("minimal") + .RunAsync(); + + if (exitCode != 0) + { + Terminal.WriteErrorLine($" ❌ Failed: {relativePath}"); + failedCount++; + } + else + { + Terminal.WriteLine($" ✅ Success: {relativePath}"); + } + } + + if (failedCount > 0) + { + Environment.ExitCode = 1; + Terminal.WriteErrorLine($"\n❌ {failedCount} sample(s) failed to compile"); + return Value; + } + + Terminal.WriteLine($"\n✅ All {sampleFiles.Length} sample(s) verified successfully!"); + return Value; + } + } +} diff --git a/tools/dev-cli/endpoints/workflow.cs b/tools/dev-cli/endpoints/workflow.cs index d253405..97a43b6 100644 --- a/tools/dev-cli/endpoints/workflow.cs +++ b/tools/dev-cli/endpoints/workflow.cs @@ -8,6 +8,10 @@ // pr/merge: clean -> build -> test // release: clean -> build -> check-version -> pack -> push +#region Purpose +// 'dev workflow' endpoint: orchestrates the CI/CD pipeline (pr/merge: clean-build-test; release adds version check, pack, and NuGet push). +#endregion + namespace DevCli.Commands; using System.Xml.Linq; @@ -145,7 +149,7 @@ private async Task RunReleaseWorkflowAsync(string? apiKey) Terminal.WriteLine("==============================================================================="); Terminal.WriteLine(" Step 3/5: Check Version"); Terminal.WriteLine("==============================================================================="); - await CheckVersionAsync(repoRoot); + await CheckVersionAsync(); // Step 4: Pack Terminal.WriteLine(""); @@ -168,38 +172,15 @@ private async Task RunReleaseWorkflowAsync(string? apiKey) Terminal.WriteLine("==============================================================================="); } - private async Task CheckVersionAsync(string repoRoot) + private async Task CheckVersionAsync() { - string propsPath = Path.Combine(repoRoot, "source", "Directory.Build.props"); + CheckVersionCommand.Handler checkVersionHandler = new(Terminal); + await checkVersionHandler.Handle(new CheckVersionCommand(), CancellationToken.None); - if (!File.Exists(propsPath)) + if (Environment.ExitCode != 0) { - throw new FileNotFoundException($"Could not find {propsPath}"); + throw new InvalidOperationException("Version check failed. Aborting release."); } - - XDocument doc = XDocument.Load(propsPath); - string? version = doc.Descendants("Version").FirstOrDefault()?.Value; - - if (string.IsNullOrEmpty(version)) - { - throw new InvalidOperationException("Could not find version in source/Directory.Build.props"); - } - - Terminal.WriteLine($"Checking if TimeWarp.Builder {version} is already published on NuGet.org..."); - - CommandOutput result = await Shell.Builder("dotnet") - .WithArguments("package", "search", "TimeWarp.Builder", "--exact-match", "--prerelease", "--source", "https://api.nuget.org/v3/index.json") - .WithNoValidation() - .CaptureAsync(); - - if (result.Stdout.Contains($"| {version} |", StringComparison.Ordinal)) - { - Terminal.WriteErrorLine($"TimeWarp.Builder {version} is already published. Increment the version in source/Directory.Build.props."); - Environment.ExitCode = 1; - throw new InvalidOperationException($"Version {version} already published."); - } - - Terminal.WriteLine($"✅ TimeWarp.Builder {version} is not yet published. Ready to release."); } private async Task PushPackagesAsync(string repoRoot, string? apiKey)