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
23 changes: 11 additions & 12 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<TValue>`, `IViewerClet`, `ICletRegistry`, `CletKind`, `CletRunResult<T>`, `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<T>` and return a typed result via `Task<CletRunResult<TValue>> RunAsync(...)`. Viewer clets are read-only (dismissable with Esc/q/Ctrl-C) and return status-only envelopes.
**Two clet kinds:** Input clets implement `ICliCommand<TValue>` and return a typed result via `Task<CommandResult<TValue>> 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`.

Expand Down
2 changes: 2 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
<TerminalGuiVersion Condition="'$(TerminalGuiVersion)' == ''">2.4.1</TerminalGuiVersion>
<!-- Pinned Terminal.Gui.Editor version. CI / release workflows can override via -p:TerminalGuiEditorVersion=<x>. -->
<TerminalGuiEditorVersion Condition="'$(TerminalGuiEditorVersion)' == ''">2.5.0</TerminalGuiEditorVersion>
<!-- Pinned Terminal.Gui.Cli version. CI / release workflows can override via -p:TerminalGuiCliVersion=<x>. -->
<TerminalGuiCliVersion Condition="'$(TerminalGuiCliVersion)' == ''">0.1.0-develop.5</TerminalGuiCliVersion>
</PropertyGroup>

</Project>
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand All @@ -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:
Expand Down
7 changes: 0 additions & 7 deletions src/Clet/Abstractions/BoxedCletResult.cs

This file was deleted.

3 changes: 0 additions & 3 deletions src/Clet/Abstractions/CletKind.cs

This file was deleted.

9 changes: 0 additions & 9 deletions src/Clet/Abstractions/CletOptionDescriptor.cs

This file was deleted.

23 changes: 0 additions & 23 deletions src/Clet/Abstractions/CletRunOptions.cs

This file was deleted.

18 changes: 0 additions & 18 deletions src/Clet/Abstractions/CletRunResult.cs

This file was deleted.

3 changes: 0 additions & 3 deletions src/Clet/Abstractions/CletRunStatus.cs

This file was deleted.

53 changes: 0 additions & 53 deletions src/Clet/Abstractions/IClet.cs

This file was deleted.

8 changes: 0 additions & 8 deletions src/Clet/Abstractions/ICletRegistry.cs

This file was deleted.

23 changes: 0 additions & 23 deletions src/Clet/Abstractions/IViewerClet.cs

This file was deleted.

1 change: 1 addition & 0 deletions src/Clet/Clet.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

<ItemGroup>
<PackageReference Include="Terminal.Gui" Version="$(TerminalGuiVersion)" />
<PackageReference Include="Terminal.Gui.Cli" Version="$(TerminalGuiCliVersion)" />
<PackageReference Include="Terminal.Gui.Editor" Version="$(TerminalGuiEditorVersion)" />
<!--
Workaround: TG's transitive TextMateSharp reference doesn't flow into
Expand Down
21 changes: 11 additions & 10 deletions src/Clet/Clets/Input/AttributePickerClet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,29 @@
using Terminal.Gui.ViewBase;
using Terminal.Gui.Views;
using TgAttribute = Terminal.Gui.Drawing.Attribute;
using Terminal.Gui.Cli;

namespace Clet;

internal sealed class AttributePickerClet : IClet<JsonObject?>
internal sealed class AttributePickerClet : ICliCommand<JsonObject?>
{
public string PrimaryAlias => "attribute-picker";
public IReadOnlyList<string> Aliases => ["attribute-picker", "attribute"];
public string Description => "Prompts for text attributes (foreground, background, style) and returns a JSON object.";
public CletKind Kind => CletKind.Input;
public CommandKind Kind => CommandKind.Input;
public Type ResultType => typeof (JsonObject);

public IReadOnlyList<CletOptionDescriptor> Options => [];
public IReadOnlyList<CommandOptionDescriptor> Options => [];

public async Task<CletRunResult<JsonObject?>> RunAsync (
public async Task<CommandResult<JsonObject?>> RunAsync (
IApplication app,
string? initial,
CletRunOptions options,
CommandRunOptions options,
CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return new () { Status = CletRunStatus.Cancelled };
return new (CommandStatus.Cancelled, default, null, null);
}

AttributePicker picker = new ();
Expand Down Expand Up @@ -54,19 +55,19 @@ internal sealed class AttributePickerClet : IClet<JsonObject?>
}
catch (OperationCanceledException)
{
return new () { Status = CletRunStatus.Cancelled };
return new (CommandStatus.Cancelled, default, null, null);
}

if (cancellationToken.IsCancellationRequested)
{
return new () { Status = CletRunStatus.Cancelled };
return new (CommandStatus.Cancelled, default, null, null);
}

TgAttribute? result = wrapper.Result;

if (result is not { } attr)
{
return new () { Status = CletRunStatus.Ok, Value = null };
return new (CommandStatus.Ok, null, null, null);
}

Color fg = attr.Foreground;
Expand All @@ -79,6 +80,6 @@ internal sealed class AttributePickerClet : IClet<JsonObject?>
["style"] = attr.Style.ToString ().ToLowerInvariant (),
};

return new () { Status = CletRunStatus.Ok, Value = obj };
return new (CommandStatus.Ok, obj, null, null);
}
}
15 changes: 8 additions & 7 deletions src/Clet/Clets/Input/ColorClet.cs
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
using Terminal.Gui.App;
using Terminal.Gui.Drawing;
using Terminal.Gui.Views;
using Terminal.Gui.Cli;

namespace Clet;

internal sealed class ColorClet : IClet<string?>
internal sealed class ColorClet : ICliCommand<string?>
{
public string PrimaryAlias => "color";
public IReadOnlyList<string> Aliases => ["color"];
public string Description => "Prompts for a color and returns a hex string (#rrggbb).";
public CletKind Kind => CletKind.Input;
public CommandKind Kind => CommandKind.Input;
public Type ResultType => typeof (string);

public IReadOnlyList<CletOptionDescriptor> Options => [];
public IReadOnlyList<CommandOptionDescriptor> Options => [];

public bool TryValidateInitial (string initial, CletRunOptions options)
public bool TryValidateInitial (string initial, CommandRunOptions options)
=> Color.TryParse (initial, null, out _);

public async Task<CletRunResult<string?>> RunAsync (
public async Task<CommandResult<string?>> RunAsync (
IApplication app,
string? initial,
CletRunOptions options,
CommandRunOptions options,
CancellationToken cancellationToken)
{
ColorPicker picker = new ();
Expand All @@ -42,7 +43,7 @@ public bool TryValidateInitial (string initial, CletRunOptions options)
{
string? hex = result is { } c ? $"#{c.R:x2}{c.G:x2}{c.B:x2}" : null;

return new () { Status = CletRunStatus.Ok, Value = hex };
return new (CommandStatus.Ok, hex, null, null);
},
addEnterBinding: false);
}
Expand Down
15 changes: 8 additions & 7 deletions src/Clet/Clets/Input/ConfirmClet.cs
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
using Terminal.Gui.App;
using Terminal.Gui.Views;
using Terminal.Gui.Cli;

namespace Clet;

internal sealed class ConfirmClet : IClet<bool?>
internal sealed class ConfirmClet : ICliCommand<bool?>
{
public string PrimaryAlias => "confirm";
public IReadOnlyList<string> Aliases => ["confirm"];
public string Description => "Prompts for a yes/no confirmation and returns a boolean.";
public CletKind Kind => CletKind.Input;
public CommandKind Kind => CommandKind.Input;
public Type ResultType => typeof (bool);

public IReadOnlyList<CletOptionDescriptor> Options => [];
public IReadOnlyList<CommandOptionDescriptor> Options => [];

public bool TryValidateInitial (string initial, CletRunOptions options)
public bool TryValidateInitial (string initial, CommandRunOptions options)
=> string.Equals (initial, "true", StringComparison.OrdinalIgnoreCase)
|| string.Equals (initial, "yes", StringComparison.OrdinalIgnoreCase)
|| string.Equals (initial, "false", StringComparison.OrdinalIgnoreCase)
|| string.Equals (initial, "no", StringComparison.OrdinalIgnoreCase);

public async Task<CletRunResult<bool?>> RunAsync (
public async Task<CommandResult<bool?>> RunAsync (
IApplication app,
string? initial,
CletRunOptions options,
CommandRunOptions options,
CancellationToken cancellationToken)
{
OptionSelector selector = new ()
Expand Down Expand Up @@ -62,7 +63,7 @@ public bool TryValidateInitial (string initial, CletRunOptions options)
_ => null,
};

return new () { Status = CletRunStatus.Ok, Value = value };
return new (CommandStatus.Ok, value, null, null);
},
addEnterBinding: false);
}
Expand Down
Loading
Loading