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..3c1da2c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -16,6 +16,8 @@ 2.4.1 2.5.0 + + 0.1.0-develop.5 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 @@ +