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
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,17 @@ jobs:
- name: Build
run: dotnet build Terminal.Gui.Cli.slnx --no-restore -c Debug

- name: Restore .NET tools
if: matrix.os == 'ubuntu-latest'
run: dotnet tool restore

- name: Verify code style
if: matrix.os == 'ubuntu-latest'
run: dotnet format Terminal.Gui.Cli.slnx --no-restore --verify-no-changes
shell: bash
run: |
dotnet jb cleanupcode Terminal.Gui.Cli.slnx --no-build --verbosity=WARN
dotnet format Terminal.Gui.Cli.slnx --no-restore
git diff --exit-code

- name: Terminal.Gui.Cli.Tests
run: dotnet run --project tests/Terminal.Gui.Cli.Tests --no-build -c Debug
Expand Down
12 changes: 12 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,18 @@ jobs:
- name: Build
run: dotnet build Terminal.Gui.Cli.slnx --no-restore -c Release -p:Version=${{ env.VERSION }}

- name: Restore .NET tools
if: matrix.os == 'ubuntu-latest'
run: dotnet tool restore

- name: Verify code style
if: matrix.os == 'ubuntu-latest'
shell: bash
run: |
dotnet jb cleanupcode Terminal.Gui.Cli.slnx --no-build --verbosity=WARN
dotnet format Terminal.Gui.Cli.slnx --no-restore
git diff --exit-code

- name: Terminal.Gui.Cli.Tests
run: dotnet run --project tests/Terminal.Gui.Cli.Tests --no-build -c Release

Expand Down
34 changes: 26 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ This file provides guidance to AI coding agents working in this repository.

## Project status

This repository currently contains scaffolding only for `Terminal.Gui.Cli`.
No library implementation is present yet.
This repository implements the `Terminal.Gui.Cli` library — a hosting layer
for Terminal.Gui apps that provides CLI parsing, dispatch, JSON output, and
AI-agent discoverability.

## Source of truth

Expand All @@ -24,12 +25,27 @@ If guidance conflicts, follow `specs/constitution.md`.

## Build and test

- `dotnet restore Terminal.Gui.Cli.slnx`
- `dotnet build Terminal.Gui.Cli.slnx --no-restore -c Debug`
- `dotnet format Terminal.Gui.Cli.slnx --no-restore --verify-no-changes`
- `dotnet run --project tests/Terminal.Gui.Cli.Tests --no-build -c Debug`
- `dotnet run --project tests/Terminal.Gui.Cli.IntegrationTests --no-build -c Debug`
- `dotnet run --project tests/Terminal.Gui.Cli.SmokeTests --no-build -c Debug`
```bash
dotnet restore Terminal.Gui.Cli.slnx
dotnet build Terminal.Gui.Cli.slnx --no-restore -c Debug
dotnet run --project tests/Terminal.Gui.Cli.Tests --no-build -c Debug
dotnet run --project tests/Terminal.Gui.Cli.IntegrationTests --no-build -c Debug
dotnet run --project tests/Terminal.Gui.Cli.SmokeTests --no-build -c Debug
```

## Code style verification (must pass before committing)

CI runs JetBrains ReSharper cleanup (`dotnet jb cleanupcode`) followed by
`dotnet format` and checks for a clean `git diff`. To replicate locally:

```bash
dotnet tool restore
dotnet jb cleanupcode Terminal.Gui.Cli.slnx --no-build --verbosity=WARN
dotnet format Terminal.Gui.Cli.slnx --no-restore
git diff --exit-code # must produce no output
```

Run these commands **before committing** to avoid CI failures.

## Coding standards

Expand All @@ -39,4 +55,6 @@ If guidance conflicts, follow `specs/constitution.md`.
- Use Allman braces and always include braces for conditionals/loops.
- Prefer guard clauses / early returns to deep nesting.
- One type per file for non-trivial public/internal types.
- **Use target-typed `new()`** when the type is clear from context (ReSharper enforces this).
- **Do not add redundant `using` directives** — if a namespace matches the file's own namespace prefix, it's already in scope. ReSharper will remove these.
- Do not add unrelated implementation while scaffolding.
13 changes: 13 additions & 0 deletions dotnet-tools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"jetbrains.resharper.globaltools": {
"version": "2026.1.2",
"commands": [
"jb"
],
"rollForward": false
}
}
}
46 changes: 46 additions & 0 deletions examples/Terminal.Gui.Cli.ExampleApp/GreetCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Terminal.Gui.App;

namespace Terminal.Gui.Cli.ExampleApp;

/// <summary>An input command that prompts for a name and returns a greeting.</summary>
public sealed class GreetCommand : ICliCommand<string>
{
/// <inheritdoc />
public string PrimaryAlias => "greet";

/// <inheritdoc />
public IReadOnlyList<string> Aliases { get; } = ["greet"];

/// <inheritdoc />
public string Description => "Prompt for a name and return a greeting.";

/// <inheritdoc />
public CommandKind Kind => CommandKind.Input;

/// <inheritdoc />
public Type ResultType => typeof (string);

/// <inheritdoc />
public IReadOnlyList<CommandOptionDescriptor> Options { get; } =
[
new ("formal", "f", typeof (bool), "Use a formal greeting style.", false, null)
];

/// <inheritdoc />
public Task<CommandResult<string>> RunAsync (
IApplication app,
string? initial,
CommandRunOptions options,
CancellationToken cancellationToken)
{
var name = initial ?? "World";
var formal = options.CommandOptions.TryGetValue ("formal", out var formalValue)
&& formalValue.Equals ("true", StringComparison.OrdinalIgnoreCase);

var greeting = formal
? $"Good day, {name}. It is a pleasure to meet you."
: $"Hello, {name}!";

return Task.FromResult (new CommandResult<string> (CommandStatus.Ok, greeting, null, null));
}
}
57 changes: 57 additions & 0 deletions examples/Terminal.Gui.Cli.ExampleApp/InfoCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using Terminal.Gui.App;

namespace Terminal.Gui.Cli.ExampleApp;

/// <summary>A viewer command that displays application information.</summary>
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
""";

/// <inheritdoc />
public string PrimaryAlias => "info";

/// <inheritdoc />
public IReadOnlyList<string> Aliases { get; } = ["info"];

/// <inheritdoc />
public string Description => "Display application information.";

/// <inheritdoc />
public CommandKind Kind => CommandKind.Viewer;

/// <inheritdoc />
public Type ResultType => typeof (string);

/// <inheritdoc />
public IReadOnlyList<CommandOptionDescriptor> Options { get; } = [];

/// <inheritdoc />
public Task<CommandResult> RunAsync (
IApplication app,
string? initial,
CommandRunOptions options,
CancellationToken cancellationToken)
{
return Task.FromResult (new CommandResult (CommandStatus.Ok, InfoText, null, null));
}

/// <inheritdoc />
public Task<CommandResult?> RenderCatAsync (
CommandRunOptions options,
TextWriter stdout,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull (stdout);
stdout.Write (InfoText);
return Task.FromResult<CommandResult?> (new CommandResult (CommandStatus.Ok, null, null, null));
}
}
18 changes: 17 additions & 1 deletion examples/Terminal.Gui.Cli.ExampleApp/Program.cs
Original file line number Diff line number Diff line change
@@ -1 +1,17 @@
Console.WriteLine ("Terminal.Gui.Cli scaffold example app.");
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);
65 changes: 65 additions & 0 deletions examples/Terminal.Gui.Cli.ExampleApp/Resources/agent-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Example App Agent Guide

This document describes how AI agents should interact with `example-app`.

## Available Commands

### greet

An input command that prompts the user for their name and returns a greeting.

- **Alias:** `greet`
- **Kind:** Input
- **Result type:** `string`
- **Options:**
- `--formal` / `-f`: Use a formal greeting style (flag).

**Usage:**

```bash
example-app greet --initial "World"
example-app greet --initial "World" --json
example-app greet --initial "World" --formal
```

### info

A viewer command that displays application information.

- **Alias:** `info`
- **Kind:** Viewer
- **Result type:** `string`
- **Supports `--cat`:** Yes

**Usage:**

```bash
example-app info --cat
example-app info --cat --json
```

## Framework Options

All commands support these framework options:

| Option | Description |
|--------|-------------|
| `--help` / `-h` | Show help |
| `--version` | Show version |
| `--opencli` | Emit OpenCLI metadata JSON |
| `--json` | Emit JSON envelope output |
| `--initial <value>` | Pre-fill input value |
| `--timeout <duration>` | Cancel after duration (e.g., `30s`, `5m`) |
| `--output <path>` / `-o` | Write output to file |

## JSON Envelope

All commands support `--json` to emit structured output:

```json
{
"schemaVersion": 1,
"status": "ok",
"value": "Hello, World!"
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@
<ProjectReference Include="..\..\src\Terminal.Gui.Cli\Terminal.Gui.Cli.csproj" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="Resources\agent-guide.md" LogicalName="Terminal.Gui.Cli.ExampleApp.agent-guide.md" />
</ItemGroup>

</Project>
8 changes: 6 additions & 2 deletions specs/constitution.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,16 @@ For versioned machine-readable contracts, `v1` schemas are append-only. Existing

Warnings are treated as errors. Builds and CI must remain warning-free.

## IV. Testing Tiers
## IV. Public API Layout

Public API is documented in `specs/library-spec.md`. Two narrow file-layout exceptions are intentional: `CommandResult` and `CommandResult<T>` live together in `CommandResult.cs`, and `ICliCommand<TValue>` lives in `ICliCommandGeneric.cs`. Do not use angle brackets in filenames; the `Generic` suffix is the established convention for this single generic-interface companion file.

## V. Testing Tiers

- `Terminal.Gui.Cli.Tests` — unit tests
- `Terminal.Gui.Cli.IntegrationTests` — integration tests
- `Terminal.Gui.Cli.SmokeTests` — smoke-level validation

## V. Governance
## VI. Governance

Constitution changes require a pull request that updates this file and explains the rationale and migration impact.
15 changes: 15 additions & 0 deletions specs/library-spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Terminal.Gui.Cli Library Specification

This repository implements the `Terminal.Gui.Cli` package API described by issue "Terminal.Gui.Cli Library Specification".

Public API additions must keep the following contracts aligned with implementation:

- Command model: `CommandKind`, `CommandStatus`, `CommandOptionDescriptor`, `CommandResult`, and `CommandResult<T>`.
- Command interfaces: `ICliCommand`, `ICliCommand<TValue>`, and `IViewerCommand`.
- Registry: `ICommandRegistry` and `CommandRegistry` with case-insensitive alias resolution and duplicate rejection.
- Host and parser: `CliHost`, `CliHostOptions`, `CommandRunOptions`, `GlobalOptionDescriptor`, and `ArgParser`.
- Help and built-ins: `IHelpProvider`, `MetadataHelpProvider`, `EmbeddedMarkdownHelpProvider`, `HelpCommand`, and `AgentGuideCommand`.
- Output and metadata: `JsonEnvelope`, `ResultWriter`, `OpenCliWriter`, `ExitCodes`, `TypeNames`, `TerminalEscapeSanitizer`, and `MarkdownRenderer`.
- Input helper: `InputCommandRunner`.

`CommandResult` and `CommandResult<T>` intentionally live together in `CommandResult.cs`. `ICliCommand<TValue>` intentionally lives in `ICliCommandGeneric.cs`; do not use angle brackets in filenames.
49 changes: 49 additions & 0 deletions src/Terminal.Gui.Cli/AgentGuideCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Terminal.Gui.App;

namespace Terminal.Gui.Cli;

/// <summary>Non-interactive viewer command that prints the consumer's agent guide.</summary>
public sealed class AgentGuideCommand : IViewerCommand
{
private readonly string _markdown;

/// <summary>Creates an agent guide command from resolved markdown content.</summary>
public AgentGuideCommand (string markdown)
{
_markdown = markdown ?? throw new ArgumentNullException (nameof (markdown));
}

/// <inheritdoc />
public string PrimaryAlias => "agent-guide";

/// <inheritdoc />
public IReadOnlyList<string> Aliases { get; } = ["agent-guide"];

/// <inheritdoc />
public string Description => "Show the agent guide.";

/// <inheritdoc />
public CommandKind Kind => CommandKind.Viewer;

/// <inheritdoc />
public Type ResultType => typeof (string);

/// <inheritdoc />
public IReadOnlyList<CommandOptionDescriptor> Options { get; } = [];

/// <inheritdoc />
public Task<CommandResult> RunAsync (IApplication app, string? initial, CommandRunOptions options,
CancellationToken cancellationToken)
{
return Task.FromResult (new CommandResult (CommandStatus.Ok, _markdown, null, null));
}

/// <inheritdoc />
public Task<CommandResult?> RenderCatAsync (CommandRunOptions options, TextWriter stdout,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull (stdout);
stdout.Write (_markdown);
return Task.FromResult<CommandResult?> (new CommandResult (CommandStatus.Ok, null, null, null));
}
}
Loading
Loading