diff --git a/CLAUDE.md b/CLAUDE.md
index 63c2c87..0df42db 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -21,7 +21,7 @@ If guidance conflicts, follow `specs/constitution.md`.
- `tests/Terminal.Gui.Cli.Tests` — unit tests
- `tests/Terminal.Gui.Cli.IntegrationTests` — integration tests
- `tests/Terminal.Gui.Cli.SmokeTests` — smoke tests
-- `examples/Terminal.Gui.Cli.ExampleApp` — sample console app
+- `examples/greet` — sample console app
## Build and test
diff --git a/Directory.Build.props b/Directory.Build.props
index f0681d4..5ab21bf 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -12,15 +12,25 @@
gui-cs
Copyright (c) gui-cs and contributors
- 0.1.0-develop
+ 0.2.1-develop
https://github.com/gui-cs/cli
https://github.com/gui-cs/cli
git
LICENSE
- 2.4.1-develop.11
+ 2.4.3
+
+
+ true
+ true
+ true
+ snupkg
+
+
+
+
diff --git a/README.md b/README.md
index c54e767..761cfc5 100644
--- a/README.md
+++ b/README.md
@@ -1,32 +1,35 @@
# Terminal.Gui.Cli
-
+[](https://www.nuget.org/packages/Terminal.Gui.Cli)
+[](LICENSE)
-A .NET library that lets [Terminal.Gui](https://github.com/gui-cs/Terminal.Gui) applications expose Views as scriptable CLI commands with typed JSON output, POSIX exit codes, and AI-agent discoverability.
+> A .NET library that turns [Terminal.Gui](https://github.com/gui-cs/Terminal.Gui) apps into scriptable CLI tools — with typed JSON output, POSIX exit codes, and built-in AI-agent discoverability.
-Ships as a single NuGet package: **[`Terminal.Gui.Cli`](https://www.nuget.org/packages/Terminal.Gui.Cli)**.
+
-## What it does
+## Why
-`Terminal.Gui.Cli` provides a hosting layer (`CliHost`) that wires up:
+Terminal.Gui gives you rich TUI applications. **Terminal.Gui.Cli** lets those same apps participate in scripts, pipelines, and agentic workflows — no separate CLI layer needed.
-- **CLI parsing** — positional command dispatch, typed options, `--initial` pre-fill for input commands.
-- **Structured output** — `--json` emits a versioned `JsonEnvelope`; `--cat` renders viewer content headlessly.
-- **AI-agent discoverability** — `--opencli` emits machine-readable metadata; `--agent-guide` serves embedded Markdown guidance.
-- **Built-in help** — `--help` renders command/option metadata via pluggable `IHelpProvider`.
-- **Exit codes** — deterministic POSIX exit codes from `CommandResult` status.
+One NuGet package. One `CliHost`. All your views become commands.
-## Command model
+## Features
-| Kind | Interface | Description |
-|------|-----------|-------------|
-| **Input** | `ICliCommand` | Launches a Terminal.Gui UI, returns a typed result. |
-| **Viewer** | `IViewerCommand` | Displays content; supports `--cat` for headless rendering. |
-
-Commands register explicitly (no reflection scanning) and resolve by case-insensitive alias.
+| Capability | How |
+|---|---|
+| **CLI parsing** | Positional command dispatch, typed options, `--initial` pre-fill |
+| **Structured output** | `--json` emits a versioned `JsonEnvelope` |
+| **Headless rendering** | `--cat` renders viewer content without a TUI |
+| **AI discoverability** | `--opencli` metadata + `agent-guide` embedded Markdown |
+| **Built-in help** | `--help` via pluggable `IHelpProvider` |
+| **Exit codes** | Deterministic POSIX codes from `CommandResult` |
## Quickstart
+```sh
+dotnet add package Terminal.Gui.Cli
+```
+
```csharp
using Terminal.Gui.Cli;
@@ -36,29 +39,33 @@ CliHost host = new (options =>
options.Version = "1.0.0";
});
-host.Registry.Register (new MyCommand ());
+host.Registry.Register (new GreetCommand ());
return await host.RunAsync (args);
```
+Then run it:
+
```sh
-# Interactive (launches Terminal.Gui)
-my-app greet --initial "World"
+my-app greet --initial "World" # interactive TUI
+my-app greet --initial "World" --json # → {"schemaVersion":1,"status":"ok","value":"Hello, World!"}
+my-app info --cat # headless viewer output
+my-app --opencli # machine-readable command metadata
+my-app agent-guide # embedded agent guidance (Markdown)
+```
-# JSON envelope
-my-app greet --initial "World" --json
+## Command model
-# Agent discovery
-my-app --opencli
-my-app agent-guide
+| Kind | Interface | Description |
+|------|-----------|-------------|
+| **Input** | `ICliCommand` | Launches a Terminal.Gui UI, returns a typed result |
+| **Viewer** | `IViewerCommand` | Displays content; supports `--cat` for headless rendering |
-# Headless viewer
-my-app info --cat
-```
+Commands register explicitly (no reflection scanning) and resolve by case-insensitive alias.
-## Framework options
+## Global options
-All commands inherit these options from the host:
+Every command inherits these from the host:
| Option | Description |
|--------|-------------|
@@ -66,7 +73,7 @@ All commands inherit these options from the host:
| `--version` | Show version |
| `--opencli` | Emit OpenCLI metadata JSON |
| `--json` | Wrap output in JSON envelope |
-| `--initial ` | Pre-fill input value |
+| `--initial ` | Pre-fill input value (non-interactive mode) |
| `--timeout ` | Cancel after duration (e.g., `30s`, `5m`) |
| `--output ` / `-o` | Write output to file |
| `--cat` | Headless render (viewer commands only) |
@@ -74,35 +81,39 @@ All commands inherit these options from the host:
## Repository layout
```
-specs/ Constitution and library spec
src/ Terminal.Gui.Cli library
tests/ Unit, integration, and smoke tests
-examples/ Example console app
+examples/ Example console app (see hero GIF above)
+specs/ Constitution and library specification
scripts/ Tooling and recording scripts
docs/ Images and documentation assets
```
-## Build
+## Building from source
-Requires .NET 10 SDK. Solution file: `Terminal.Gui.Cli.slnx`.
+Requires **.NET 10 SDK**. Solution: `Terminal.Gui.Cli.slnx`.
```sh
dotnet restore Terminal.Gui.Cli.slnx
dotnet build Terminal.Gui.Cli.slnx
-# Tests
+# Run all test tiers
dotnet run --project tests/Terminal.Gui.Cli.Tests
dotnet run --project tests/Terminal.Gui.Cli.IntegrationTests
dotnet run --project tests/Terminal.Gui.Cli.SmokeTests
-# Example app
-dotnet run --project examples/Terminal.Gui.Cli.ExampleApp -- greet --initial "World" --json
+# Try the example app
+dotnet run --project examples/greet -- greet --initial "World" --json
```
## Status
-**Alpha** — `0.1.0-develop` pre-release stream on the `develop` branch.
+**Alpha** — `0.1.0-develop` pre-release. API surface is stabilizing; breaking changes possible.
+
+## Contributing
+
+See [`specs/constitution.md`](specs/constitution.md) for architectural rules and PR requirements.
## License
-MIT; see [`LICENSE`](LICENSE).
+MIT — see [`LICENSE`](LICENSE).
diff --git a/Terminal.Gui.Cli.slnx b/Terminal.Gui.Cli.slnx
index 2a828b3..f78cb7c 100644
--- a/Terminal.Gui.Cli.slnx
+++ b/Terminal.Gui.Cli.slnx
@@ -16,7 +16,7 @@
-
+
diff --git a/docs/images/hero.gif b/docs/images/hero.gif
index da61867..a3670fa 100644
Binary files a/docs/images/hero.gif and b/docs/images/hero.gif differ
diff --git a/examples/Terminal.Gui.Cli.ExampleApp/InfoCommand.cs b/examples/Terminal.Gui.Cli.ExampleApp/InfoCommand.cs
deleted file mode 100644
index b9331d2..0000000
--- a/examples/Terminal.Gui.Cli.ExampleApp/InfoCommand.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-using Terminal.Gui.App;
-
-namespace Terminal.Gui.Cli.ExampleApp;
-
-/// A viewer command that displays application information.
-public sealed class InfoCommand : IViewerCommand
-{
- private const string InfoText = """
- Example App v1.0.0
- A demonstration of the Terminal.Gui.Cli library.
-
- This app shows how to:
- - Register input and viewer commands
- - Use --help, --json, --opencli, and agent-guide
- - Embed an agent-guide.md resource
- - Support --cat for headless content rendering
- """;
-
- ///
- public string PrimaryAlias => "info";
-
- ///
- public IReadOnlyList Aliases { get; } = ["info"];
-
- ///
- public string Description => "Display application information.";
-
- ///
- public CommandKind Kind => CommandKind.Viewer;
-
- ///
- public Type ResultType => typeof (string);
-
- ///
- public IReadOnlyList Options { get; } = [];
-
- ///
- public Task RunAsync (
- IApplication app,
- string? initial,
- CommandRunOptions options,
- CancellationToken cancellationToken)
- {
- return Task.FromResult (new CommandResult (CommandStatus.Ok, InfoText, null, null));
- }
-
- ///
- public Task RenderCatAsync (
- CommandRunOptions options,
- TextWriter stdout,
- CancellationToken cancellationToken)
- {
- ArgumentNullException.ThrowIfNull (stdout);
- stdout.Write (InfoText);
- return Task.FromResult (new CommandResult (CommandStatus.Ok, null, null, null));
- }
-}
diff --git a/examples/Terminal.Gui.Cli.ExampleApp/Program.cs b/examples/Terminal.Gui.Cli.ExampleApp/Program.cs
deleted file mode 100644
index e241dff..0000000
--- a/examples/Terminal.Gui.Cli.ExampleApp/Program.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using System.Reflection;
-using Terminal.Gui.Cli;
-using Terminal.Gui.Cli.ExampleApp;
-
-CliHost host = new (options =>
-{
- options.ApplicationName = "example-app";
- options.Version = "1.0.0";
- options.AgentGuide = "Terminal.Gui.Cli.ExampleApp.agent-guide.md";
- options.AgentGuideIsResource = true;
- options.ResourceAssembly = Assembly.GetExecutingAssembly ();
-});
-
-host.Registry.Register (new GreetCommand ());
-host.Registry.Register (new InfoCommand ());
-
-return await host.RunAsync (args);
diff --git a/examples/Terminal.Gui.Cli.ExampleApp/Terminal.Gui.Cli.ExampleApp.csproj b/examples/Terminal.Gui.Cli.ExampleApp/Terminal.Gui.Cli.ExampleApp.csproj
deleted file mode 100644
index 3737551..0000000
--- a/examples/Terminal.Gui.Cli.ExampleApp/Terminal.Gui.Cli.ExampleApp.csproj
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
- Exe
- Terminal.Gui.Cli.ExampleApp
- Terminal.Gui.Cli.ExampleApp
- false
-
-
-
-
-
-
-
-
-
-
-
diff --git a/examples/greet/FarewellCommand.cs b/examples/greet/FarewellCommand.cs
new file mode 100644
index 0000000..5e269fc
--- /dev/null
+++ b/examples/greet/FarewellCommand.cs
@@ -0,0 +1,52 @@
+using Terminal.Gui.App;
+
+namespace Terminal.Gui.Cli.Greet;
+
+/// An input command that says goodbye to someone.
+public sealed class FarewellCommand : ICliCommand
+{
+ ///
+ public string PrimaryAlias => "farewell";
+
+ ///
+ public IReadOnlyList Aliases { get; } = ["farewell", "bye"];
+
+ ///
+ public string Description => "Say goodbye to someone.";
+
+ ///
+ public CommandKind Kind => CommandKind.Input;
+
+ ///
+ public Type ResultType => typeof (string);
+
+ ///
+ public IReadOnlyList Options { get; } =
+ [
+ new ("until", "u", typeof (string), "When you expect to meet again.", false, null)
+ ];
+
+ ///
+ public bool AcceptsPositionalArgs => true;
+
+ ///
+ public Task> RunAsync (
+ IApplication app,
+ string? initial,
+ CommandRunOptions options,
+ CancellationToken cancellationToken)
+ {
+ var name = options.Arguments.Count > 0
+ ? string.Join (" ", options.Arguments)
+ : initial ?? "World";
+ var until = options.CommandOptions.TryGetValue ("until", out var untilValue)
+ ? untilValue
+ : null;
+
+ var farewell = until is not null
+ ? $"Goodbye, {name}! See you {until}."
+ : $"Goodbye, {name}!";
+
+ return Task.FromResult (new CommandResult (CommandStatus.Ok, farewell, null, null));
+ }
+}
diff --git a/examples/Terminal.Gui.Cli.ExampleApp/GreetCommand.cs b/examples/greet/GreetCommand.cs
similarity index 85%
rename from examples/Terminal.Gui.Cli.ExampleApp/GreetCommand.cs
rename to examples/greet/GreetCommand.cs
index f0308ca..766d164 100644
--- a/examples/Terminal.Gui.Cli.ExampleApp/GreetCommand.cs
+++ b/examples/greet/GreetCommand.cs
@@ -1,6 +1,6 @@
using Terminal.Gui.App;
-namespace Terminal.Gui.Cli.ExampleApp;
+namespace Terminal.Gui.Cli.Greet;
/// An input command that prompts for a name and returns a greeting.
public sealed class GreetCommand : ICliCommand
@@ -26,6 +26,9 @@ public sealed class GreetCommand : ICliCommand
new ("formal", "f", typeof (bool), "Use a formal greeting style.", false, null)
];
+ ///
+ public bool AcceptsPositionalArgs => true;
+
///
public Task> RunAsync (
IApplication app,
@@ -33,7 +36,10 @@ public Task> RunAsync (
CommandRunOptions options,
CancellationToken cancellationToken)
{
- var name = initial ?? "World";
+ var name = options.Arguments.Count > 0
+ ? string.Join (" ", options.Arguments)
+ : initial ?? "World";
+
var formal = options.CommandOptions.TryGetValue ("formal", out var formalValue)
&& formalValue.Equals ("true", StringComparison.OrdinalIgnoreCase);
diff --git a/examples/greet/InfoCommand.cs b/examples/greet/InfoCommand.cs
new file mode 100644
index 0000000..82ce38c
--- /dev/null
+++ b/examples/greet/InfoCommand.cs
@@ -0,0 +1,102 @@
+using Terminal.Gui.App;
+using Terminal.Gui.Input;
+using Terminal.Gui.ViewBase;
+using Terminal.Gui.Views;
+
+namespace Terminal.Gui.Cli.Greet;
+
+/// A viewer command that displays application information in a TUI markdown view.
+public sealed class InfoCommand : IViewerCommand
+{
+ private const string InfoMarkdown = """
+ # greet — Info
+
+ **Version:** 1.0.0
+
+ A demonstration of the `Terminal.Gui.Cli` library.
+
+ ## Features
+
+ This app shows how to:
+
+ - Register input and viewer commands
+ - Use `--help`, `--json`, `--opencli`, and `agent-guide`
+ - Embed an `agent-guide.md` resource
+ - Support `--cat` for headless content rendering
+ - Launch a TUI markdown viewer for `help` and `info`
+
+ ## Usage
+
+ ```
+ greet [name] Greet someone (default: World)
+ greet --formal [name] Use a formal greeting style
+ greet help Browse help topics
+ greet help greet Help for the greet command
+ greet info Show this info page
+ greet --help Render help as ANSI to stdout
+ ```
+ """;
+
+ ///
+ public string PrimaryAlias => "info";
+
+ ///
+ public IReadOnlyList Aliases { get; } = ["info"];
+
+ ///
+ public string Description => "Display application information.";
+
+ ///
+ public CommandKind Kind => CommandKind.Viewer;
+
+ ///
+ public Type ResultType => typeof (void);
+
+ ///
+ public IReadOnlyList Options { get; } = [];
+
+ ///
+ public async Task RunAsync (
+ IApplication app,
+ string? initial,
+ CommandRunOptions options,
+ CancellationToken cancellationToken)
+ {
+ Runnable window = new ()
+ {
+ Title = "Info",
+ Width = Dim.Fill (),
+ Height = Dim.Fill ()
+ };
+
+ Markdown markdownView = new ()
+ {
+ Width = Dim.Fill (),
+ Height = Dim.Fill (1)
+ };
+
+ StatusBar statusBar = new (
+ [
+ new Shortcut (Application.GetDefaultKey (Command.Quit), "Quit", window.RequestStop)
+ ]);
+
+ window.Add (markdownView, statusBar);
+
+ window.Initialized += (_, _) => { markdownView.Text = InfoMarkdown; };
+
+ await app.RunAsync (window, cancellationToken);
+
+ return new CommandResult (CommandStatus.Ok, null, null, null);
+ }
+
+ ///
+ public Task RenderCatAsync (
+ CommandRunOptions options,
+ TextWriter stdout,
+ CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull (stdout);
+ MarkdownRenderer.RenderToAnsi (InfoMarkdown, stdout);
+ return Task.FromResult (new CommandResult (CommandStatus.Ok, null, null, null));
+ }
+}
diff --git a/examples/greet/Program.cs b/examples/greet/Program.cs
new file mode 100644
index 0000000..2c8480d
--- /dev/null
+++ b/examples/greet/Program.cs
@@ -0,0 +1,22 @@
+using System.Reflection;
+using Terminal.Gui.Cli;
+using Terminal.Gui.Cli.Greet;
+
+Assembly assembly = Assembly.GetExecutingAssembly ();
+
+CliHost host = new (options =>
+{
+ options.ApplicationName = "greet";
+ options.Version = "1.0.0";
+ options.DefaultCommand = "greet";
+ options.AgentGuide = "Terminal.Gui.Cli.Greet.agent-guide.md";
+ options.AgentGuideIsResource = true;
+ options.ResourceAssembly = assembly;
+ options.HelpProvider = new EmbeddedMarkdownHelpProvider (assembly);
+});
+
+host.Registry.Register (new GreetCommand ());
+host.Registry.Register (new FarewellCommand ());
+host.Registry.Register (new InfoCommand ());
+
+return await host.RunAsync (args);
diff --git a/examples/greet/README.md b/examples/greet/README.md
new file mode 100644
index 0000000..c441e3c
--- /dev/null
+++ b/examples/greet/README.md
@@ -0,0 +1,51 @@
+# greet
+
+A sample CLI app built with `Terminal.Gui.Cli` that generates greetings.
+
+## Usage
+
+```bash
+# Launch the TUI greeting prompt
+greet greet
+
+# Provide a name directly (headless)
+greet greet --initial "Alice"
+
+# Formal greeting style
+greet greet --initial "Bob" --formal
+
+# JSON envelope output
+greet greet --initial "World" --json
+
+# Show help in the TUI markdown viewer
+greet help
+
+# Render help as ANSI markdown to stdout
+greet help --cat
+
+# Show root help (ANSI markdown to stdout)
+greet --help
+
+# Show version
+greet --version
+```
+
+## Commands
+
+| Command | Description |
+|---------|-------------|
+| `greet` | Prompt for a name and return a greeting. |
+| `info` | Display application information. |
+| `help` | Show command help in a TUI markdown viewer. |
+
+## Building
+
+```bash
+dotnet build examples/greet/Terminal.Gui.Cli.Greet.csproj
+```
+
+## Running
+
+```bash
+dotnet run --project examples/greet -- greet --initial "World"
+```
diff --git a/examples/greet/Resources/Help/farewell.md b/examples/greet/Resources/Help/farewell.md
new file mode 100644
index 0000000..20e2ae7
--- /dev/null
+++ b/examples/greet/Resources/Help/farewell.md
@@ -0,0 +1,36 @@
+# farewell
+
+Say goodbye to someone.
+
+[Back to main help](help:help)
+
+## Usage
+
+```
+farewell [name]
+farewell --until
+
+
+ <_Parameter1>GoldensSourcePath
+ <_Parameter2>$(MSBuildProjectDirectory)\Goldens
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Terminal.Gui.Cli.Tests/AssemblyAttributes.cs b/tests/Terminal.Gui.Cli.Tests/AssemblyAttributes.cs
new file mode 100644
index 0000000..6cdafbb
--- /dev/null
+++ b/tests/Terminal.Gui.Cli.Tests/AssemblyAttributes.cs
@@ -0,0 +1,3 @@
+using Xunit;
+
+[assembly: CollectionBehavior (DisableTestParallelization = true)]
diff --git a/tests/Terminal.Gui.Cli.Tests/CliHostTests.cs b/tests/Terminal.Gui.Cli.Tests/CliHostTests.cs
index ae35150..dfb1791 100644
--- a/tests/Terminal.Gui.Cli.Tests/CliHostTests.cs
+++ b/tests/Terminal.Gui.Cli.Tests/CliHostTests.cs
@@ -43,6 +43,52 @@ public async Task RunAsync_AgentGuideCat_WritesLiteralWithoutStartingTui ()
Assert.Equal (string.Empty, stderr.ToString ());
}
+ [Fact]
+ public async Task RunAsync_HelpFlag_RendersMarkdownAsAnsi ()
+ {
+ CliHost host = new (options =>
+ {
+ options.ApplicationName = "sample";
+ options.Version = "1.0.0";
+ });
+ using StringWriter stdout = new ();
+ using StringWriter stderr = new ();
+
+ var exitCode = await host.RunAsync (["--help"], TestContext.Current.CancellationToken, stdout, stderr);
+
+ Assert.Equal (ExitCodes.Ok, exitCode);
+ Assert.Equal (string.Empty, stderr.ToString ());
+
+ var output = stdout.ToString ();
+
+ // ANSI escape sequences should be present (rendered markdown)
+ Assert.Contains ("\x1b[", output);
+
+ // Should contain the command name from the registry
+ Assert.Contains ("help", output);
+ }
+
+ [Fact]
+ public async Task RunAsync_HelpCat_RendersMarkdownAsAnsi ()
+ {
+ CliHost host = new ();
+ using StringWriter stdout = new ();
+ using StringWriter stderr = new ();
+
+ var exitCode = await host.RunAsync (["help", "--cat"], TestContext.Current.CancellationToken, stdout, stderr);
+
+ Assert.Equal (ExitCodes.Ok, exitCode);
+ Assert.Equal (string.Empty, stderr.ToString ());
+
+ var output = stdout.ToString ();
+
+ // ANSI escape sequences should be present (rendered markdown)
+ Assert.Contains ("\x1b[", output);
+
+ // Should contain the command name from the registry
+ Assert.Contains ("help", output);
+ }
+
[Fact]
public async Task RunAsync_CommandCancellation_ReturnsCancelledExitCode ()
{
diff --git a/tests/Terminal.Gui.Cli.Tests/MarkdownRendererTests.cs b/tests/Terminal.Gui.Cli.Tests/MarkdownRendererTests.cs
new file mode 100644
index 0000000..c7973ae
--- /dev/null
+++ b/tests/Terminal.Gui.Cli.Tests/MarkdownRendererTests.cs
@@ -0,0 +1,52 @@
+using Xunit;
+
+namespace Terminal.Gui.Cli.Tests;
+
+[Collection ("MarkdownRenderer")]
+public sealed class MarkdownRendererTests
+{
+ [Fact]
+ public void RenderToAnsi_RestoresDisableRealDriverIO_WhenUnset ()
+ {
+ // Arrange: clear the env var
+ Environment.SetEnvironmentVariable ("DisableRealDriverIO", null);
+
+ using StringWriter output = new ();
+
+ // Act
+ MarkdownRenderer.RenderToAnsi ("# Hello", output);
+
+ // Assert: should be restored to null (unset)
+ var after = Environment.GetEnvironmentVariable ("DisableRealDriverIO");
+ Assert.Null (after);
+ }
+
+ [Fact]
+ public void RenderToAnsi_RestoresDisableRealDriverIO_WhenPreviouslySet ()
+ {
+ // Arrange: set the env var to a known value
+ Environment.SetEnvironmentVariable ("DisableRealDriverIO", "0");
+
+ using StringWriter output = new ();
+
+ // Act
+ MarkdownRenderer.RenderToAnsi ("# Hello", output);
+
+ // Assert: should restore to the previous value, not leave it as "1"
+ var after = Environment.GetEnvironmentVariable ("DisableRealDriverIO");
+ Assert.Equal ("0", after);
+
+ // Cleanup
+ Environment.SetEnvironmentVariable ("DisableRealDriverIO", null);
+ }
+
+ [Fact]
+ public void RenderToAnsi_ProducesOutput ()
+ {
+ using StringWriter output = new ();
+
+ MarkdownRenderer.RenderToAnsi ("**bold**", output);
+
+ Assert.NotEmpty (output.ToString ());
+ }
+}
diff --git a/tests/Terminal.Gui.Cli.Tests/MetadataHelpProviderTests.cs b/tests/Terminal.Gui.Cli.Tests/MetadataHelpProviderTests.cs
new file mode 100644
index 0000000..6b331df
--- /dev/null
+++ b/tests/Terminal.Gui.Cli.Tests/MetadataHelpProviderTests.cs
@@ -0,0 +1,64 @@
+using Terminal.Gui.App;
+using Xunit;
+
+namespace Terminal.Gui.Cli.Tests;
+
+public sealed class MetadataHelpProviderTests
+{
+ [Fact]
+ public void GetRootHelp_ProducesMarkdown ()
+ {
+ MetadataHelpProvider provider = new ();
+ CommandRegistry registry = new ();
+ registry.Register (new StubCommand ("demo", "A demo command."));
+
+ var result = provider.GetRootHelp (registry);
+
+ Assert.NotNull (result);
+ Assert.Contains ("## Commands", result);
+ Assert.Contains ("| `demo` | A demo command. |", result);
+ Assert.Contains ("## Framework Options", result);
+ Assert.Contains ("| `--help`, `-h` | Show help |", result);
+ }
+
+ [Fact]
+ public void GetCommandHelp_ProducesMarkdown ()
+ {
+ MetadataHelpProvider provider = new ();
+ StubCommand command = new ("test", "Test command.");
+
+ var result = provider.GetCommandHelp (command);
+
+ Assert.NotNull (result);
+ Assert.Contains ("# test", result);
+ Assert.Contains ("Test command.", result);
+ }
+
+ private sealed class StubCommand : ICliCommand
+ {
+ public StubCommand (string alias, string description)
+ {
+ PrimaryAlias = alias;
+ Aliases = [alias];
+ Description = description;
+ }
+
+ public string PrimaryAlias { get; }
+
+ public IReadOnlyList Aliases { get; }
+
+ public string Description { get; }
+
+ public CommandKind Kind => CommandKind.Input;
+
+ public Type ResultType => typeof (void);
+
+ public IReadOnlyList Options { get; } = [];
+
+ public Task RunAsync (IApplication app, string? initial,
+ CommandRunOptions options, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException ();
+ }
+ }
+}
diff --git a/tests/Terminal.Gui.Cli.Tests/OutputTests.cs b/tests/Terminal.Gui.Cli.Tests/OutputTests.cs
index d4a88d0..66a8d8e 100644
--- a/tests/Terminal.Gui.Cli.Tests/OutputTests.cs
+++ b/tests/Terminal.Gui.Cli.Tests/OutputTests.cs
@@ -1,4 +1,5 @@
using System.Text.Json;
+using System.Text.Json.Serialization;
using Xunit;
namespace Terminal.Gui.Cli.Tests;
@@ -17,6 +18,20 @@ public void JsonEnvelope_ToJson_UsesCamelCaseAndOmitsNulls ()
Assert.False (document.RootElement.TryGetProperty ("code", out _));
}
+ [Fact]
+ public void JsonEnvelope_ToJson_WithResolver_EmbedsConsumerTypeAsObject ()
+ {
+ var json = JsonEnvelope.Ok (new SampleResult ("Alice", 30, null))
+ .ToJson (SampleJsonContext.Default);
+
+ using JsonDocument document = JsonDocument.Parse (json);
+ JsonElement value = document.RootElement.GetProperty ("value");
+ Assert.Equal (JsonValueKind.Object, value.ValueKind);
+ Assert.Equal ("Alice", value.GetProperty ("name").GetString ());
+ Assert.Equal (30, value.GetProperty ("age").GetInt32 ());
+ Assert.False (value.TryGetProperty ("note", out _));
+ }
+
[Fact]
public void ResultWriter_WritesErrorsToStderrInPlainText ()
{
@@ -39,3 +54,11 @@ public void TerminalEscapeSanitizer_RemovesOscAndPreservesRenderedSgr ()
TerminalEscapeSanitizer.SanitizeRenderedOutput ("\u001b[1mstrong\u001b[0m"));
}
}
+
+internal sealed record SampleResult (string Name, int Age, string? Note);
+
+[JsonSourceGenerationOptions (
+ PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
+[JsonSerializable (typeof (SampleResult))]
+internal sealed partial class SampleJsonContext : JsonSerializerContext;