From 6254bfda6d865f07fa6e637d446c29f6480fdc4c Mon Sep 17 00:00:00 2001 From: Tig Date: Sun, 24 May 2026 21:41:45 -0600 Subject: [PATCH 1/3] Adopt Terminal.Gui.Cli package, replacing internal CLI infrastructure Replace clet's internal Abstractions/, Registry/, Json/, and Hosting infrastructure (CommandLineRoot, AliasDispatcher, OutputFormatter, ExitCodes, etc.) with the Terminal.Gui.Cli NuGet package (0.1.0-develop.2). Key changes: - Add Terminal.Gui.Cli 0.1.0-develop.2 PackageReference - Delete src/Clet/Abstractions/, Registry/, Json/ directories - Delete hosting files replaced by CliHost (CommandLineRoot, AliasDispatcher, OutputFormatter, ExitCodes, CletTypeNames, TerminalEscapeSanitizer, MarkdownHelpRenderer, HelpClet) - Rewrite Program.cs to use CliHost from the package - Create BuiltInCommands.cs, CletHelpProvider.cs, CletOptionsExtensions.cs - Update all 14 input clets and 3 viewer clets to use package types (ICliCommand, CommandResult, CommandStatus, CommandRunOptions) - Clet-specific options (--allow-file, --allow-binary, --no-browse) registered as GlobalOptionDescriptor, accessed via Extensions dictionary - Delete 7 unit test files for package-owned infrastructure - Update all remaining test files for new type names - Update CLAUDE.md architecture section and README.md Resolves #180 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CLAUDE.md | 23 +- Directory.Build.props | 2 + README.md | 10 +- src/Clet/Abstractions/BoxedCletResult.cs | 7 - src/Clet/Abstractions/CletKind.cs | 3 - src/Clet/Abstractions/CletOptionDescriptor.cs | 9 - src/Clet/Abstractions/CletRunOptions.cs | 23 - src/Clet/Abstractions/CletRunResult.cs | 18 - src/Clet/Abstractions/CletRunStatus.cs | 3 - src/Clet/Abstractions/IClet.cs | 53 -- src/Clet/Abstractions/ICletRegistry.cs | 8 - src/Clet/Abstractions/IViewerClet.cs | 23 - src/Clet/Clet.csproj | 1 + src/Clet/Clets/Input/AttributePickerClet.cs | 21 +- src/Clet/Clets/Input/ColorClet.cs | 15 +- src/Clet/Clets/Input/ConfirmClet.cs | 15 +- src/Clet/Clets/Input/DateClet.cs | 15 +- src/Clet/Clets/Input/DecimalClet.cs | 15 +- src/Clet/Clets/Input/DurationClet.cs | 15 +- src/Clet/Clets/Input/InputCletRunner.cs | 29 +- src/Clet/Clets/Input/IntClet.cs | 15 +- src/Clet/Clets/Input/LinearRangeClet.cs | 55 +- src/Clet/Clets/Input/MultiSelectClet.cs | 23 +- src/Clet/Clets/Input/PickDirectoryClet.cs | 23 +- src/Clet/Clets/Input/PickFileClet.cs | 29 +- src/Clet/Clets/Input/SelectClet.cs | 19 +- src/Clet/Clets/Input/TextClet.cs | 19 +- src/Clet/Clets/Input/TimeClet.cs | 15 +- src/Clet/Clets/Viewer/ConfigClet.cs | 19 +- src/Clet/Clets/Viewer/EditorClet.cs | 25 +- src/Clet/Clets/Viewer/HelpClet.cs | 247 -------- src/Clet/Clets/Viewer/MarkdownClet.cs | 63 +- src/Clet/Hosting/AliasDispatcher.cs | 123 ---- .../BuiltInCommands.cs} | 11 +- src/Clet/Hosting/CletHelpProvider.cs | 103 ++++ src/Clet/Hosting/CletOptionsExtensions.cs | 22 + src/Clet/Hosting/CletTypeNames.cs | 76 --- src/Clet/Hosting/CommandLineRoot.cs | 574 ------------------ src/Clet/Hosting/ExitCodes.cs | 30 - src/Clet/Hosting/MarkdownContentResolver.cs | 14 +- src/Clet/Hosting/MarkdownHelpRenderer.cs | 246 -------- src/Clet/Hosting/OutputFormatter.cs | 111 ---- src/Clet/Hosting/Program.cs | 28 +- src/Clet/Hosting/TerminalEscapeSanitizer.cs | 234 ------- src/Clet/Json/CletJsonContext.cs | 17 - src/Clet/Json/SchemaV1.cs | 39 -- src/Clet/Registry/CletRegistry.cs | 27 - .../AttributePickerCletIntegrationTests.cs | 14 +- .../ColorCletIntegrationTests.cs | 20 +- .../ConfigCletIntegrationTests.cs | 14 +- .../ConfirmCletIntegrationTests.cs | 20 +- .../DateCletIntegrationTests.cs | 20 +- .../DecimalCletIntegrationTests.cs | 20 +- .../DurationCletIntegrationTests.cs | 20 +- .../IntCletIntegrationTests.cs | 20 +- .../LinearRangeCletIntegrationTests.cs | 26 +- .../MarkdownCletIntegrationTests.cs | 56 +- .../MultiSelectCletIntegrationTests.cs | 26 +- .../PickDirectoryCletIntegrationTests.cs | 14 +- .../PickFileCletIntegrationTests.cs | 14 +- .../SelectCletIntegrationTests.cs | 26 +- .../TextCletIntegrationTests.cs | 32 +- .../TimeCletIntegrationTests.cs | 20 +- tests/Clet.SmokeTests/CletSmokeTests.cs | 26 +- tests/Clet.UITests/CletUiHarness.cs | 24 +- tests/Clet.UITests/CletUiTests.cs | 31 +- .../AttributePickerCletTests.cs | 6 +- tests/Clet.UnitTests/BuiltInCletsTests.cs | 35 +- tests/Clet.UnitTests/CletMetadataTests.cs | 50 +- tests/Clet.UnitTests/CletRegistryTests.cs | 71 --- tests/Clet.UnitTests/CletRunResultTests.cs | 48 -- tests/Clet.UnitTests/CletTypeNamesTests.cs | 68 --- tests/Clet.UnitTests/ColorCletTests.cs | 8 +- tests/Clet.UnitTests/CommandLineRootTests.cs | 498 --------------- tests/Clet.UnitTests/ConfirmCletTests.cs | 6 +- tests/Clet.UnitTests/DateCletTests.cs | 6 +- tests/Clet.UnitTests/DecimalCletTests.cs | 6 +- tests/Clet.UnitTests/DurationCletTests.cs | 6 +- tests/Clet.UnitTests/ExitCodesTests.cs | 18 +- tests/Clet.UnitTests/IntCletTests.cs | 8 +- tests/Clet.UnitTests/LinearRangeCletTests.cs | 18 +- tests/Clet.UnitTests/MarkdownCletTests.cs | 4 +- .../MarkdownContentResolverTests.cs | 18 +- tests/Clet.UnitTests/MultiSelectCletTests.cs | 4 +- tests/Clet.UnitTests/OutputFormatterTests.cs | 308 ---------- .../Clet.UnitTests/PickDirectoryCletTests.cs | 6 +- tests/Clet.UnitTests/PickFileCletTests.cs | 6 +- tests/Clet.UnitTests/SchemaV1Tests.cs | 118 ---- tests/Clet.UnitTests/SelectCletTests.cs | 4 +- .../TerminalEscapeSanitizerTests.cs | 241 -------- tests/Clet.UnitTests/TextCletTests.cs | 6 +- tests/Clet.UnitTests/TimeCletTests.cs | 6 +- 92 files changed, 776 insertions(+), 3755 deletions(-) delete mode 100644 src/Clet/Abstractions/BoxedCletResult.cs delete mode 100644 src/Clet/Abstractions/CletKind.cs delete mode 100644 src/Clet/Abstractions/CletOptionDescriptor.cs delete mode 100644 src/Clet/Abstractions/CletRunOptions.cs delete mode 100644 src/Clet/Abstractions/CletRunResult.cs delete mode 100644 src/Clet/Abstractions/CletRunStatus.cs delete mode 100644 src/Clet/Abstractions/IClet.cs delete mode 100644 src/Clet/Abstractions/ICletRegistry.cs delete mode 100644 src/Clet/Abstractions/IViewerClet.cs delete mode 100644 src/Clet/Clets/Viewer/HelpClet.cs delete mode 100644 src/Clet/Hosting/AliasDispatcher.cs rename src/Clet/{Registry/BuiltInClets.cs => Hosting/BuiltInCommands.cs} (74%) create mode 100644 src/Clet/Hosting/CletHelpProvider.cs create mode 100644 src/Clet/Hosting/CletOptionsExtensions.cs delete mode 100644 src/Clet/Hosting/CletTypeNames.cs delete mode 100644 src/Clet/Hosting/CommandLineRoot.cs delete mode 100644 src/Clet/Hosting/ExitCodes.cs delete mode 100644 src/Clet/Hosting/MarkdownHelpRenderer.cs delete mode 100644 src/Clet/Hosting/OutputFormatter.cs delete mode 100644 src/Clet/Hosting/TerminalEscapeSanitizer.cs delete mode 100644 src/Clet/Json/CletJsonContext.cs delete mode 100644 src/Clet/Json/SchemaV1.cs delete mode 100644 src/Clet/Registry/CletRegistry.cs delete mode 100644 tests/Clet.UnitTests/CletRegistryTests.cs delete mode 100644 tests/Clet.UnitTests/CletRunResultTests.cs delete mode 100644 tests/Clet.UnitTests/CletTypeNamesTests.cs delete mode 100644 tests/Clet.UnitTests/CommandLineRootTests.cs delete mode 100644 tests/Clet.UnitTests/OutputFormatterTests.cs delete mode 100644 tests/Clet.UnitTests/SchemaV1Tests.cs delete mode 100644 tests/Clet.UnitTests/TerminalEscapeSanitizerTests.cs diff --git a/CLAUDE.md b/CLAUDE.md index 0096b79..dd54535 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -60,30 +60,29 @@ There is no separate lint step. CI runs on ubuntu-latest with `dotnet-quality: p Projects in this repo: -- **`src/Clet/`** — The CLI executable (net10.0). Depends on `Terminal.Gui` v2 (preview NuGet, currently `2.0.2-develop.24` — pin tracked in `src/Clet/Clet.csproj`, must be replaced with a release tag before v0.5 schema-lock per spec §8 risks). All abstractions are `internal` (not published until v2 plugin system). `BuiltInClets.RegisterAll` registers the shipped clets by hand; auto-discovery via a source generator was explored and dropped. -- **`tests/Clet.UnitTests/`** — Registry, JSON schema, host pipeline (CommandLineRoot, OutputFormatter, ExitCodes, BuiltInClets) tests. +- **`src/Clet/`** — The CLI executable (net10.0). Depends on `Terminal.Gui.Cli` (preview NuGet, version pinned in `Directory.Build.props`) which provides the CLI hosting infrastructure (`CliHost`, `CommandRegistry`, `ResultWriter`, `ExitCodes`, `JsonEnvelope`, etc.). clet is a thin consumer: it registers its commands via `BuiltInCommands.RegisterAll` and lets the package handle parsing, dispatch, and output formatting. Auto-discovery via a source generator was explored and dropped. +- **`tests/Clet.UnitTests/`** — Command implementations, file access policy, content resolution, and BuiltInCommands registration tests. - **`tests/Clet.ConfigTests/`** — Non-parallel assembly for all `ConfigurationManager`-touching tests (EditorSettings, FileAccessSettings CM round-trips). `xunit.runner.json` disables both assembly and collection parallelization. **Never enable CM in the other parallel test projects.** - **`tests/Clet.IntegrationTests/`** — In-process tests that init Terminal.Gui (`Application.Create()`, `app.Init("ansi")`). - **`tests/Clet.SmokeTests/`** — Process-level smoke tests (`Process.Start` against the built `Clet.dll`). The keystroke-driven cases land at v0.3 with TUIcast — see `specs/decisions.md` D-007. ### Key directory layout inside `src/Clet/` -- `Abstractions/` — `IClet`, `IClet`, `IViewerClet`, `ICletRegistry`, `CletKind`, `CletRunResult`, `CletRunOptions`, `CletOptionDescriptor`, `BoxedCletResult` (non-generic dispatch type — see decisions D-005) -- `Registry/` — `CletRegistry` (instance-based, case-insensitive alias lookup, duplicate protection); `BuiltInClets.RegisterAll` (manual registration) -- `Json/` — `SchemaV1` (the JSON envelope) and `CletJsonContext` (source-generated System.Text.Json) -- `Clets/Input/` — Input clet implementations (currently `SelectClet`) -- `Clets/Viewer/` — Viewer/browser clet implementations (`md`, `help`) -- `Hosting/` — `Program.cs` entry point, `CommandLineRoot` (hand-rolled CLI parser; D-006), `AliasDispatcher`, `OutputFormatter`, `ExitCodes` +- `Clets/Input/` — Input command implementations (SelectClet, TextClet, IntClet, etc.) +- `Clets/Viewer/` — Viewer/browser command implementations (`md`, `edit`, `config`) +- `Hosting/` — `Program.cs` entry point (uses `CliHost` from `Terminal.Gui.Cli`), `BuiltInCommands` (registration helper), `CletHelpProvider` (custom help), `CletOptionsExtensions` (extension accessors for clet-specific global options) ### Core patterns -**Two clet kinds:** Input clets wrap a View with `IValue` and return a typed result via `Task> RunAsync(...)`. Viewer clets are read-only (dismissable with Esc/q/Ctrl-C) and return status-only envelopes. +**Two clet kinds:** Input clets implement `ICliCommand` and return a typed result via `Task> RunAsync(...)`. Viewer clets implement `IViewerCommand` (dismissable with Esc/q/Ctrl-C) and return status-only envelopes. -**JSON result envelope (SchemaV1):** `{ schemaVersion: 1, status, value?, code?, message? }`. Status is one of: `ok`, `cancelled`, `error`, `no-result`. Value is omitted (not null) when absent. +**JSON result envelope:** `{ schemaVersion: 1, status, value?, code?, message? }`. Status is one of: `ok`, `cancelled`, `error`, `no-result`. Value is omitted (not null) when absent. Envelope format is owned by the `Terminal.Gui.Cli` package (`JsonEnvelope`). -**Exit codes:** 0 = success, 2 = usage error, 130 = cancelled (SIGINT convention). +**Exit codes:** 0 = success, 2 = usage error, 130 = cancelled (SIGINT convention). Provided by the package's `ExitCodes` class. -**Registry:** Instance-based (not static singletons). Tests can create isolated registries. +**Registry:** Instance-based (not static singletons) via the package's `CommandRegistry`. Tests can create isolated registries. + +**Clet-specific global options:** `--allow-file`, `--allow-binary`, `--no-browse` are registered as `GlobalOptionDescriptor` entries in `CliHost` configuration. Accessed via `CommandRunOptions.Extensions` dictionary using helpers in `CletOptionsExtensions`. **InternalsVisibleTo:** Both test projects have access to internals via `src/Clet/Properties/AssemblyInfo.cs`. diff --git a/Directory.Build.props b/Directory.Build.props index 7cee5a9..e286615 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -16,6 +16,8 @@ 2.4.1 2.5.0 + + 0.1.0-develop.2 diff --git a/README.md b/README.md index bcb59f9..d6a5f84 100644 --- a/README.md +++ b/README.md @@ -96,8 +96,8 @@ clet edit ./notes.txt # Open the configuration editor (theming, keybindings, etc.) clet config -# See all available clets -clet list +# See available commands (TUI help viewer) +clet help ``` ### AI agent usage (`--json`) @@ -115,9 +115,9 @@ clet pick-file --json --root ./src --timeout 30s clet confirm --json "Apply this patch?" # → {"schemaVersion":1,"status":"cancelled"} (exit 130) -# Discover available clets once per session -clet list --json -# → {"schemaVersion":1,"clets":[{"alias":"select","kind":"input","resultType":"string",...},...]} +# Discover available commands (machine-readable manifest) +clet --opencli +# → {"name":"clet","version":"...","commands":[{"alias":"select","kind":"input","resultType":"string",...},...]} ``` Exit codes: diff --git a/src/Clet/Abstractions/BoxedCletResult.cs b/src/Clet/Abstractions/BoxedCletResult.cs deleted file mode 100644 index b226df7..0000000 --- a/src/Clet/Abstractions/BoxedCletResult.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Clet; - -internal readonly record struct BoxedCletResult ( - CletRunStatus Status, - object? Value, - string? ErrorCode, - string? ErrorMessage); diff --git a/src/Clet/Abstractions/CletKind.cs b/src/Clet/Abstractions/CletKind.cs deleted file mode 100644 index e8a59c7..0000000 --- a/src/Clet/Abstractions/CletKind.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Clet; - -internal enum CletKind { Input, Viewer } diff --git a/src/Clet/Abstractions/CletOptionDescriptor.cs b/src/Clet/Abstractions/CletOptionDescriptor.cs deleted file mode 100644 index b83ceaf..0000000 --- a/src/Clet/Abstractions/CletOptionDescriptor.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Clet; - -internal sealed record CletOptionDescriptor ( - string Name, - string? ShortName, - Type ValueType, - string Description, - bool Required, - string? DefaultValue); diff --git a/src/Clet/Abstractions/CletRunOptions.cs b/src/Clet/Abstractions/CletRunOptions.cs deleted file mode 100644 index 4cd20c0..0000000 --- a/src/Clet/Abstractions/CletRunOptions.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Clet; - -internal sealed record CletRunOptions -{ - public string? Title { get; init; } - public bool JsonOutput { get; init; } - public TimeSpan? Timeout { get; init; } - public bool Fullscreen { get; init; } - public bool Cat { get; init; } - public string? OutputPath { get; init; } - public int? Rows { get; init; } - public IReadOnlyDictionary? CletOptions { get; init; } - public IReadOnlyList? Arguments { get; init; } - - /// Paths explicitly allowed for file reading (bypasses extension + cwd checks). - public IReadOnlyList? AllowedFiles { get; init; } - - /// When true, binary file content (NUL bytes) is permitted. - public bool AllowBinary { get; init; } - - /// When true, disables browser-mode navigation (back/forward) for viewer clets. - public bool NoBrowse { get; init; } -} diff --git a/src/Clet/Abstractions/CletRunResult.cs b/src/Clet/Abstractions/CletRunResult.cs deleted file mode 100644 index f4a1ab5..0000000 --- a/src/Clet/Abstractions/CletRunResult.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Clet; - -internal readonly record struct CletRunResult -{ - public CletRunStatus Status { get; init; } - public string? ErrorCode { get; init; } - public string? ErrorMessage { get; init; } -} - -internal readonly record struct CletRunResult -{ - public CletRunStatus Status { get; init; } - public T? Value { get; init; } - public string? ErrorCode { get; init; } - public string? ErrorMessage { get; init; } - - public CletRunResult ToUntyped () => new () { Status = Status, ErrorCode = ErrorCode, ErrorMessage = ErrorMessage }; -} diff --git a/src/Clet/Abstractions/CletRunStatus.cs b/src/Clet/Abstractions/CletRunStatus.cs deleted file mode 100644 index 6b94ff9..0000000 --- a/src/Clet/Abstractions/CletRunStatus.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Clet; - -internal enum CletRunStatus { Ok, Cancelled, Error, NoResult } diff --git a/src/Clet/Abstractions/IClet.cs b/src/Clet/Abstractions/IClet.cs deleted file mode 100644 index 0dfe89d..0000000 --- a/src/Clet/Abstractions/IClet.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Terminal.Gui.App; - -namespace Clet; - -internal interface IClet -{ - string PrimaryAlias { get; } - IReadOnlyList Aliases { get; } - string Description { get; } - CletKind Kind { get; } - Type ResultType { get; } - IReadOnlyList Options { get; } - - /// - /// Whether this clet consumes positional arguments. Defaults to . - /// Clets that accept positional args (e.g. select, multi-select, md) - /// should override this to return . - /// - bool AcceptsPositionalArgs => false; - - /// - /// Validates that the string can be parsed by this clet. - /// Returns if valid (or if the clet accepts any string). - /// Clets with typed parsing (int, date, color, etc.) should override to reject unparseable values. - /// - bool TryValidateInitial (string initial, CletRunOptions _) => true; - - Task RunBoxedAsync ( - IApplication app, - string? input, - CletRunOptions options, - CancellationToken cancellationToken); -} - -internal interface IClet : IClet -{ - Task> RunAsync ( - IApplication app, - string? initial, - CletRunOptions options, - CancellationToken cancellationToken); - - async Task IClet.RunBoxedAsync ( - IApplication app, - string? input, - CletRunOptions options, - CancellationToken cancellationToken) - { - CletRunResult result = await RunAsync (app, input, options, cancellationToken); - - return new (result.Status, result.Value, result.ErrorCode, result.ErrorMessage); - } -} diff --git a/src/Clet/Abstractions/ICletRegistry.cs b/src/Clet/Abstractions/ICletRegistry.cs deleted file mode 100644 index 2f75e67..0000000 --- a/src/Clet/Abstractions/ICletRegistry.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Clet; - -internal interface ICletRegistry -{ - void Register (IClet clet); - bool TryResolve (string alias, out IClet? clet); - IReadOnlyCollection All { get; } -} diff --git a/src/Clet/Abstractions/IViewerClet.cs b/src/Clet/Abstractions/IViewerClet.cs deleted file mode 100644 index 6d7ef83..0000000 --- a/src/Clet/Abstractions/IViewerClet.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Terminal.Gui.App; - -namespace Clet; - -internal interface IViewerClet : IClet -{ - Task RunAsync ( - IApplication app, - string? content, - CletRunOptions options, - CancellationToken cancellationToken); - - async Task IClet.RunBoxedAsync ( - IApplication app, - string? input, - CletRunOptions options, - CancellationToken cancellationToken) - { - CletRunResult result = await RunAsync (app, input, options, cancellationToken); - - return new (result.Status, null, result.ErrorCode, result.ErrorMessage); - } -} diff --git a/src/Clet/Clet.csproj b/src/Clet/Clet.csproj index 041771d..0e7b69d 100644 --- a/src/Clet/Clet.csproj +++ b/src/Clet/Clet.csproj @@ -30,6 +30,7 @@ + 2.5.0 - 0.1.0-develop.2 + 0.1.0-develop.5