diff --git a/specs/terminal-gui-cli-v2.md b/specs/terminal-gui-cli-v2.md new file mode 100644 index 0000000..7f1609a --- /dev/null +++ b/specs/terminal-gui-cli-v2.md @@ -0,0 +1,985 @@ +# Terminal.Gui.Cli Library Specification + +> Definitive implementation spec for `gui-cs/Terminal.Gui.cli`: a `Terminal.Gui` library that lets applications expose Views as scriptable CLI commands with typed JSON output, POSIX exit codes, and AI-agent discoverability. + +## 0. Repo Identity + +`gui-cs/Terminal.Gui.cli` is a new gui-cs library repo for the NuGet package `Terminal.Gui.Cli`. This repo/package casing split is normative: the repo slug uses lowercase `cli`; the package, assembly, and namespace use the `Terminal.Gui.Cli` identifier with the final segment spelled `Cli`. Before writing library code, scaffold the repo by cloning the structure and maintenance model of `gui-cs/Editor` (the Terminal.Gui.Editor repo): + +- `develop` is the integration branch; `main` is the stable release branch. +- Copy and adapt `specs/constitution.md`; it is the highest-authority engineering document. +- Copy and adapt `CLAUDE.md`, `AGENTS.md`, `.editorconfig`, `Directory.Build.props`, `Directory.Build.targets`, solution naming, and test conventions. +- Copy and adapt CI/CD workflows from Editor: `ci.yml`, `release.yml`, `prepare-release.yml`, and `finalize-release.yml`. +- Keep Editor's zero-warning policy, xUnit v3 executable tests, develop prerelease versioning, main/tag stable releases, and release-PR workflow. + +Repository/package identity: + +| Concern | Value | +|---------|-------| +| GitHub repo | `gui-cs/Terminal.Gui.cli` | +| Package ID | `Terminal.Gui.Cli` | +| Root namespace | `Terminal.Gui.Cli` | +| Primary TFM | `net10.0` | +| Dependencies | `Terminal.Gui` only | +| License | MIT | +| AOT | `true` | + +## 1. Problem Statement + +Many Terminal.Gui apps need the same hosting layer: parse command-line args, resolve an alias, initialize Terminal.Gui, run a View, serialize a typed result, emit predictable exit codes, and provide enough self-description for humans and AI agents. `clet` proved this pattern with 18 commands and 500+ tests, but the infrastructure should be a reusable library rather than app-specific hosting code. + +`Terminal.Gui.Cli` supplies that reusable layer. A consumer creates a `CliHost`, registers command instances, and calls `RunAsync`. The library owns framework flags, Terminal.Gui lifecycle, result writing, JSON envelope shape, exit-code mapping, help metadata, and OpenCLI introspection. Consumers own their concrete commands and any domain policies. + +## 2. Design Principles + +1. **The library owns Terminal.Gui lifecycle.** Commands receive an initialized `IApplication`; commands never call `Application.Create()`, `app.Init()`, or `Application.Init()`. +2. **No reflection; NativeAOT from day one.** Commands self-describe via properties. JSON serialization uses source generation. Registration is explicit. +3. **Registry stores instances.** Commands are constructed by the consumer and registered as instances, supporting DI, conditional registration, and plugins. +4. **Three-tier option model.** Framework flags are hard-coded in `ArgParser`; consumer globals are declared in `CliHostOptions.GlobalOptions` and land in `CommandRunOptions.Extensions`; per-command options are declared by `CommandOptionDescriptor` and land in `CommandRunOptions.CommandOptions`. +5. **JSON envelope is the stable wire contract.** `{ schemaVersion, status, value?, code?, message? }` is controlled by the library and append-only within schema v1. +6. **Exit codes are library-controlled.** Commands return `CommandResult`; the library maps status/error code to POSIX-conventional process codes. +7. **AI discoverability is first-class.** `llms.txt`, `agent-guide`, and `--opencli` are normative parts of the model. +8. **No general-purpose CLI ambitions.** This library is for Terminal.Gui-backed interactive commands, not a replacement for System.CommandLine. + +## 3. Engineering Constitution + +`specs/constitution.md`, cloned and adapted from `gui-cs/Editor`, is canonical. It must include these Terminal.Gui.Cli-specific rules: + +| Rule | Requirement | +|------|-------------| +| C1 | Only `CliHost` calls Terminal.Gui lifecycle APIs and disposes `IApplication`. | +| C2 | Public API changes require this spec to be updated in the same PR. | +| C3 | No reflection-based command discovery or runtime code generation. | +| C4 | Source-generated JSON only for library-owned JSON types. | +| C5 | All correctness tests run in parallel unless a test explicitly opts out for a process-global. | +| C6 | Commands never call `Environment.Exit`; return `CommandResult`. | +| C7 | Schema v1 is append-only within library major version 1.x. | +| C8 | Zero warnings in Debug and Release. | + +The constitution must also document two narrow file-layout exceptions proven by the clet prototype in PR #176: `CommandResult` and `CommandResult` live together in `CommandResult.cs`, and `ICliCommand` lives in `ICliCommandGeneric.cs`. Do not use angle brackets in filenames: `<` and `>` are invalid on Windows and awkward in POSIX shells. The `Generic` suffix is the established convention for this single generic-interface companion file. + +## 4. Public API Surface + +All public API lives in namespace `Terminal.Gui.Cli`. XML comments below are normative. + +### 4.1 Command model + +```csharp +namespace Terminal.Gui.Cli; + +/// The two kinds of CLI commands the library knows about. +public enum CommandKind +{ + /// An interactive command that returns a typed value. + Input, + + /// An interactive or headless command that does not return a typed result value. + Viewer +} + +/// Outcome status of a command run. +public enum CommandStatus +{ + /// The command completed successfully. + Ok, + + /// The user or caller cancelled the command. + Cancelled, + + /// The command failed. + Error, + + /// The command completed but produced no result. + NoResult +} + +/// Metadata descriptor for a per-command option. +public sealed record CommandOptionDescriptor ( + string Name, + string? ShortName, + Type ValueType, + string Description, + bool Required, + string? DefaultValue); + +/// Non-generic result for dispatch and output formatting. +public readonly record struct CommandResult ( + CommandStatus Status, + object? Value, + string? ErrorCode, + string? ErrorMessage); + +/// Typed result returned by input commands. +public readonly record struct CommandResult ( + CommandStatus Status, + T? Value, + string? ErrorCode, + string? ErrorMessage); +``` + +`CommandResult` and `CommandResult` must remain readonly record structs in one file because `ICliCommand` bridges the typed result to the non-generic dispatch result by copying the same status, value, error code, and error message fields. + +### 4.2 Command interfaces + +```csharp +using Terminal.Gui.App; + +namespace Terminal.Gui.Cli; + +/// A CLI command backed by Terminal.Gui. Implemented by consumer apps and built-ins. +public interface ICliCommand +{ + /// The canonical alias shown in help and OpenCLI output. + string PrimaryAlias { get; } + + /// All aliases that resolve to this command. Must include . + IReadOnlyList Aliases { get; } + + /// Human-readable one-line command description. + string Description { get; } + + /// The command kind. + CommandKind Kind { get; } + + /// The CLR type of the value written to the JSON envelope, or . + Type ResultType { get; } + + /// Per-command options accepted by this command. + IReadOnlyList Options { get; } + + /// Whether this command consumes positional arguments. + bool AcceptsPositionalArgs => false; + + /// + /// Validates the --initial value before Terminal.Gui starts. The default permits any value; + /// commands override this method when they need command-specific validation. + /// + bool TryValidateInitial (string initial, CommandRunOptions options) => true; + + /// Runs the command after the host has initialized Terminal.Gui. + Task RunAsync ( + IApplication app, + string? initial, + CommandRunOptions options, + CancellationToken cancellationToken); +} + +/// Typed command that returns a value. +public interface ICliCommand : ICliCommand +{ + /// Runs the command and returns a typed result. + new Task> RunAsync ( + IApplication app, + string? initial, + CommandRunOptions options, + CancellationToken cancellationToken); + + async Task ICliCommand.RunAsync ( + IApplication app, + string? initial, + CommandRunOptions options, + CancellationToken cancellationToken) + { + CommandResult result = await RunAsync (app, initial, options, cancellationToken); + return new (result.Status, result.Value, result.ErrorCode, result.ErrorMessage); + } +} + +/// +/// Viewer command. Viewers can be interactive TUI commands or headless content commands, +/// but they are invoked through the viewer path and default to fullscreen when a TUI is used. +/// +public interface IViewerCommand : ICliCommand +{ + /// + /// Renders content to stdout without launching the TUI. Called when --cat is set. + /// Return null to indicate --cat is not supported and normal TUI dispatch should continue. + /// + Task RenderCatAsync ( + CommandRunOptions options, + TextWriter stdout, + CancellationToken cancellationToken) => Task.FromResult (null); +} +``` + +### 4.3 Registry + +```csharp +namespace Terminal.Gui.Cli; + +/// Manages alias-to-command lookup. +public interface ICommandRegistry +{ + /// Registers a command instance. + /// + /// Thrown when PrimaryAlias is not present in Aliases, or any alias is already registered. + /// + void Register (ICliCommand command); + + /// Resolves an alias case-insensitively. + bool TryResolve (string alias, out ICliCommand? command); + + /// All registered commands in registration order. + IReadOnlyCollection All { get; } +} + +/// Default case-insensitive, duplicate-rejecting command registry. +public sealed class CommandRegistry : ICommandRegistry +{ + /// + public void Register (ICliCommand command); + + /// + public bool TryResolve (string alias, out ICliCommand? command); + + /// + public IReadOnlyCollection All { get; } +} +``` + +`PrimaryAlias` matching is case-insensitive. Duplicate alias detection is case-insensitive. Registration failure throws `InvalidOperationException`. + +### 4.4 Run options and global option descriptors + +```csharp +namespace Terminal.Gui.Cli; + +/// Parsed options bag passed to commands. +public sealed class CommandRunOptions +{ + /// Pre-fill value for the View. + public string? Initial { get; init; } + + /// Title override for TUI chrome. --prompt/-p is an alias for --title/-t. + public string? Title { get; init; } + + /// Whether to emit the JSON envelope instead of plain text. + public bool JsonOutput { get; init; } + + /// Cancel after this duration. + public TimeSpan? Timeout { get; init; } + + /// Force fullscreen. Input commands otherwise default to inline. + public bool Fullscreen { get; init; } + + /// Render supported viewer content to stdout instead of launching the TUI. + public bool Cat { get; init; } + + /// Write successful command output to this file instead of stdout. + public string? OutputPath { get; init; } + + /// Constrain inline height. + public int? Rows { get; init; } + + /// Positional arguments after the alias. + public IReadOnlyList Arguments { get; init; } = []; + + /// Per-command option values keyed by long option name without dashes. + public IReadOnlyDictionary CommandOptions { get; init; } + = new Dictionary (); + + /// Consumer-registered global option values keyed by long option name without dashes. + public IReadOnlyDictionary> Extensions { get; init; } + = new Dictionary> (); + + /// Gets the last value for a single-value consumer extension, parsed by . + public T? GetExtension (string key, Func parser, T? defaultValue = default); + + /// Gets all values for a repeatable consumer extension. + public IReadOnlyList GetExtensionList (string key); + + /// Returns true when a consumer extension flag or value is present. + public bool HasExtension (string key); +} + +/// Describes a consumer-defined global option. +public sealed record GlobalOptionDescriptor ( + string Name, + string? ShortName, + string Description, + bool IsFlag, + bool Repeatable = false); +``` + +### 4.5 Host + +```csharp +using System.Reflection; + +namespace Terminal.Gui.Cli; + +/// The main entry point. Owns parsing, dispatch, Terminal.Gui lifecycle, and output. +public sealed class CliHost +{ + /// Creates a host, applies configuration, creates its registry, and registers built-ins. + public CliHost (Action? configure = null); + + /// The command registry owned by this host. Register consumer commands before RunAsync. + public ICommandRegistry Registry { get; } + + /// Parses args, dispatches a command, writes output, and returns a process exit code. + public Task RunAsync ( + string[] args, + CancellationToken cancellationToken = default, + TextWriter? stdout = null, + TextWriter? stderr = null); +} + +/// Configuration options for . +public sealed class CliHostOptions +{ + /// Application name shown in help, version output, and OpenCLI. + public string ApplicationName { get; set; } = "app"; + + /// Version string shown in --version and OpenCLI. Null uses 0.0.0. + public string? Version { get; set; } + + /// Custom help provider. Null uses . + public IHelpProvider? HelpProvider { get; set; } + + /// Maximum characters accepted by --initial. Default is 64 KiB. + public int MaxInitialChars { get; set; } = 64 * 1024; + + /// Agent guide embedded resource name or literal markdown. Null disables agent-guide. + public string? AgentGuide { get; set; } + + /// True when is an embedded resource name; false when literal content. + public bool AgentGuideIsResource { get; set; } = true; + + /// Assembly used to resolve embedded resources. Null falls back to . + public Assembly? ResourceAssembly { get; set; } + + /// Consumer-defined global options parsed into . + public List GlobalOptions { get; } = []; + + /// Replaces a library built-in command before it is registered. + /// Thrown when is not a replaceable built-in alias. + /// Thrown when the same built-in alias is replaced more than once. + public void ReplaceBuiltInCommand (string alias, ICliCommand replacement); +} +``` + +`CliHost` constructs and owns its `CommandRegistry`. It registers built-ins during construction after applying options: `help` is always registered unless replaced; `agent-guide` is registered only when `AgentGuide` is non-null unless replaced. When `AgentGuideIsResource` is true, `CliHost` resolves `AgentGuide` from `ResourceAssembly ?? Assembly.GetEntryAssembly()` during construction and passes the resolved markdown string to `AgentGuideCommand`; missing assembly or missing resource throws `InvalidOperationException`. When `AgentGuideIsResource` is false, `AgentGuide` is already literal markdown. `ReplaceBuiltInCommand` supports reserved aliases `help` and `agent-guide`; a replacement for `help` must include `help` in `Aliases`, and a replacement for `agent-guide` must include `agent-guide` in `Aliases`. + +### 4.6 Parser + +`ArgParser` is public because the proven API contract includes direct parser tests and consumers may validate argument behavior without running a TUI. It remains a small, data-driven parser, not a general CLI framework. + +```csharp +namespace Terminal.Gui.Cli; + +/// Data-driven parser for framework flags, consumer globals, and per-command options. +public sealed class ArgParser +{ + /// Creates a parser with registered consumer globals and an --initial limit. + public ArgParser (List globalOptions, int maxInitialChars = 64 * 1024); + + /// Parses command-line arguments, optionally validating against a resolved command. + public ParseResult Parse (string[] args, ICliCommand? command = null); + + /// Parses duration strings accepted by --timeout: ms, s, m, h. + public static bool TryParseTimeout (string input, out TimeSpan timeout); + + /// Represents the result of parsing arguments. + public sealed class ParseResult + { + /// True if parsing succeeded. + public bool Success { get; init; } + + /// Error message when parsing failed. + public string? Error { get; init; } + + /// The command alias, when this is not a root flag. + public string? Alias { get; init; } + + /// The parsed initial value. + public string? Initial { get; init; } + + /// The parsed options bag. + public CommandRunOptions? Options { get; init; } + + /// Root flag detected before command dispatch. + public RootFlag? RootFlag { get; init; } + + /// Creates a failed parse result. + public static ParseResult Fail (string error); + } + + /// Root flags that exit without command dispatch. + public enum RootFlag + { + /// Root --help or -h. + Help, + + /// Root --version. + Version, + + /// Root --opencli. + OpenCli + } +} +``` + +### 4.7 Help and built-in commands + +```csharp +using System.Reflection; +using Terminal.Gui.App; + +namespace Terminal.Gui.Cli; + +/// Pluggable help rendering. +public interface IHelpProvider +{ + /// Renders root-level help. Return null to use generated fallback text. + string? GetRootHelp (ICommandRegistry registry); + + /// Renders per-command help. Return null to use generated fallback text. + string? GetCommandHelp (ICliCommand command); +} + +/// Generates help text from registry metadata. +public sealed class MetadataHelpProvider : IHelpProvider +{ + /// + public string? GetRootHelp (ICommandRegistry registry); + + /// + public string? GetCommandHelp (ICliCommand command); +} + +/// Reads embedded markdown resources for root, command, and agent help. +public sealed class EmbeddedMarkdownHelpProvider : IHelpProvider +{ + /// Creates a provider that reads markdown resources from . + public EmbeddedMarkdownHelpProvider (Assembly resourceAssembly); + + /// + public string? GetRootHelp (ICommandRegistry registry); + + /// + public string? GetCommandHelp (ICliCommand command); + + /// Reads an embedded markdown resource by exact manifest resource name. + public string? GetMarkdownResource (string resourceName); +} + +/// Interactive TUI markdown help viewer, with --cat support for ANSI stdout. +public sealed class HelpCommand : IViewerCommand +{ + /// Creates a help command that lazily reads command metadata from . + public HelpCommand (ICommandRegistry registry, IHelpProvider helpProvider); + + /// + public string PrimaryAlias { get; } + + /// + public IReadOnlyList Aliases { get; } + + /// + public string Description { get; } + + /// + public CommandKind Kind { get; } + + /// + public Type ResultType { get; } + + /// + public IReadOnlyList Options { get; } + + /// + public bool AcceptsPositionalArgs { get; } + + /// + public Task RunAsync (IApplication app, string? initial, CommandRunOptions options, CancellationToken cancellationToken); + + /// + public Task RenderCatAsync (CommandRunOptions options, TextWriter stdout, CancellationToken cancellationToken); +} + +/// Non-interactive viewer command that prints the consumer's agent guide. +public sealed class AgentGuideCommand : IViewerCommand +{ + /// Creates an agent guide command from resolved markdown content. + public AgentGuideCommand (string markdown); + + /// + public string PrimaryAlias { get; } + + /// + public IReadOnlyList Aliases { get; } + + /// + public string Description { get; } + + /// + public CommandKind Kind { get; } + + /// + public Type ResultType { get; } + + /// + public IReadOnlyList Options { get; } + + /// + public Task RunAsync (IApplication app, string? initial, CommandRunOptions options, CancellationToken cancellationToken); + + /// + public Task RenderCatAsync (CommandRunOptions options, TextWriter stdout, CancellationToken cancellationToken); +} +``` + +`HelpCommand` uses `MarkdownRenderer` for ANSI output and `Terminal.Gui.Views.Markdown` for interactive TUI mode. `AgentGuideCommand` is headless: it returns the guide markdown as the command value so plain output prints the text and `--json` wraps it in the envelope. + +### 4.8 Output and JSON + +```csharp +namespace Terminal.Gui.Cli; + +/// The stable wire format for CLI output. +public sealed class JsonEnvelope +{ + /// Wire schema version. Always 1 for library major version 1.x. + public int SchemaVersion { get; init; } = 1; + + /// Status string: ok, cancelled, error, or no-result. + public string Status { get; init; } = "ok"; + + /// Result value. Omitted when null. + public object? Value { get; init; } + + /// Error code. Omitted when null. + public string? Code { get; init; } + + /// Error message. Omitted when null. + public string? Message { get; init; } + + /// Creates an ok envelope. + public static JsonEnvelope Ok (object? value = null); + + /// Creates a cancelled envelope. + public static JsonEnvelope Cancelled (); + + /// Creates an error envelope. + public static JsonEnvelope Error (string code, string message); + + /// Creates a no-result envelope. + public static JsonEnvelope NoResult (); + + /// Serializes using the source-generated JSON context. + public string ToJson (); +} + +/// Formats command results to stdout, stderr, or an output file. +public static class ResultWriter +{ + /// Writes and returns false when output file creation fails. + public static bool Write (CommandResult result, bool jsonOutput, TextWriter stdout, TextWriter stderr, string? outputPath = null); +} + +/// Generates an OpenCLI JSON document from registry metadata. +public static class OpenCliWriter +{ + /// Generates OpenCLI JSON for the registered commands and framework options. + public static string Generate (ICommandRegistry registry, CliHostOptions options); +} +``` + +`OpenCliWriter` hand-builds JSON with a shared string-escape helper. It must escape command aliases, descriptions, option names, short names, app names, versions, and metadata values. + +The JSON source-generation context is internal, not public API: + +```csharp +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Terminal.Gui.Cli; + +[JsonSourceGenerationOptions ( + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] +[JsonSerializable (typeof (JsonEnvelope))] +internal partial class CliJsonContext : JsonSerializerContext +{ +} +``` + +### 4.9 Utilities + +```csharp +namespace Terminal.Gui.Cli; + +/// POSIX-conventional exit codes. +public static class ExitCodes +{ + /// Success. + public const int Ok = 0; + + /// Successful command execution with no result. + public const int NoResult = 1; + + /// Usage error: bad command, bad option, or output-file creation failure. + public const int UsageError = 2; + + /// Validation error, equivalent to sysexits EX_DATAERR. + public const int ValidationError = 65; + + /// I/O error, equivalent to sysexits EX_IOERR. + public const int IoError = 74; + + /// Cancelled, equivalent to 128 + SIGINT. + public const int Cancelled = 130; + + /// Maps a command result to a process exit code. + public static int FromResult (CommandResult result); +} + +/// Maps CLR types to stable wire-format type names. +public static class TypeNames +{ + /// Returns the wire-format name for . + public static string WireName (Type type); +} + +/// Strips dangerous terminal escape sequences from untrusted content. +public static class TerminalEscapeSanitizer +{ + /// Sanitizes user-supplied content before it reaches a terminal driver. + public static string? Sanitize (string? input); + + /// Sanitizes rendered ANSI, preserving only SGR CSI sequences generated by trusted renderers. + public static string SanitizeRenderedOutput (string renderedAnsi); +} + +/// Markdown-to-ANSI helper for help and viewer output. +public static class MarkdownRenderer +{ + /// Renders markdown as ANSI to and sanitizes rendered output. + public static void RenderToAnsi (string markdown, TextWriter output); +} +``` + +`MarkdownRenderer` wraps `Terminal.Gui.Views.Markdown.RenderToAnsi()` and then applies `TerminalEscapeSanitizer.SanitizeRenderedOutput` before writing to the target `TextWriter`. The required Terminal.Gui version is listed in the Terminal.Gui Dependency Floor section. + +### 4.10 InputCommandRunner + +```csharp +using Terminal.Gui.App; +using Terminal.Gui.ViewBase; +using Terminal.Gui.Views; + +namespace Terminal.Gui.Cli; + +/// Shared boilerplate for input commands that wrap a control in RunnableWrapper. +public static class InputCommandRunner +{ + /// Configures, runs, and maps the result from an input command wrapper when raw result and output value differ. + public static Task> RunAsync ( + IApplication app, + RunnableWrapper wrapper, + CommandRunOptions options, + string defaultTitle, + CancellationToken cancellationToken, + Func> resultMapper, + bool addEnterBinding = true) + where TControl : View, new(); + + /// Configures and runs a wrapper whose raw result is already the output value. + public static Task> RunAsync ( + IApplication app, + RunnableWrapper wrapper, + CommandRunOptions options, + string defaultTitle, + CancellationToken cancellationToken, + bool addEnterBinding = true) + where TControl : View, new(); +} +``` + +Use the three-type-parameter overload when the wrapper's raw result must be mapped to a different output value. Use the two-type-parameter overload when `TRawResult` and `TValue` are the same and the wrapper result can be returned directly. + +`InputCommandRunner` applies these defaults before `app.RunAsync`: `Title = options.Title ?? defaultTitle`, `Width = Dim.Fill()`, `BorderStyle = LineStyle.Rounded`, `Border.Thickness = new Thickness (0, 1, 0, 0)`, and Enter key binding to `Command.Accept` when requested. Consumers override using standard Terminal.Gui lifecycle, especially `wrapper.Initialized`; the library must not add custom styling callbacks. + +## 5. CLI Grammar + +```text + [--help|-h] + --version + --opencli + [positional...] [framework-options] [consumer-global-options] [per-command-options] + --help|-h|help + help [alias] [--cat] + agent-guide [--json] +``` + +There is no `list` command. Human listing is `--help`; structured listing is `--opencli`. + +### 5.1 Framework command flags + +| Flag | Short | Value | Target property | Behavior | +|------|-------|-------|-----------------|----------| +| `--json` | `-j` | none | `JsonOutput=true` | Write JSON envelope. | +| `--initial` | `-i` | string | `Initial` | Pre-fill command value; max 64 KiB by default. | +| `--title` | `-t` | string | `Title` | Override TUI title. | +| `--prompt` | `-p` | string | `Title` | Exact alias for `--title` / `-t`. | +| `--timeout` | none | duration | `Timeout` | Positive number with `ms`, `s`, `m`, or `h`. | +| `--fullscreen` | `-f` | none | `Fullscreen=true` | Force fullscreen app model. | +| `--cat` | none | none | `Cat=true` | Ask viewer to render to stdout without TUI. | +| `--output` | `-o` | path | `OutputPath` | Write successful command output to a newly-created file. | +| `--rows` | `-r` | positive int | `Rows` | Constrain inline height. | + +### 5.2 Root-only framework flags + +These are intercepted before command dispatch and do not populate `CommandRunOptions`. + +| Flag | Short | Behavior | +|------|-------|----------| +| `--help` | `-h` | Write root help and exit 0. | +| `--version` | none | Write app name/version and exit 0. | +| `--opencli` | none | Emit OpenCLI JSON and exit 0. | + +Supported syntax: `--option value`, `--option=value`, the short forms of framework command flags documented above, and `--` to end option parsing. Short bundling (`-jf`) is not supported. + +### 5.3 Consumer global options + +Consumers add `GlobalOptionDescriptor` values to `CliHostOptions.GlobalOptions`. Matching is case-insensitive by long name or one-character short name. For flags, `Extensions[name]` contains one empty string per occurrence. For repeatable value options, all values are appended. For non-repeatable value options, the last value wins. + +### 5.4 Per-command options + +A token not matched as a framework flag or consumer global is accepted as a per-command option only if it matches the resolved command's `Options` by `Name` or `ShortName`. Values are strings. Unknown options fail with exit code 2. Required/default semantics are metadata the command must enforce; the parser validates presence and value consumption but does not coerce to `ValueType`. + +### 5.5 Help interception + +` --help`, ` -h`, and ` help` are intercepted by `CliHost.RunAsync` before `ArgParser.Parse()`. This is required because `--help` is not a per-command option and the parser would otherwise reject it. The command must already be registered; unknown aliases return usage error. + +## 6. Execution Pipeline + +```mermaid +sequenceDiagram + participant Main as Program.Main + participant Host as CliHost + participant Parser as ArgParser + participant Registry as CommandRegistry + participant TG as Terminal.Gui + participant Command as ICliCommand + participant Writer as ResultWriter/OpenCliWriter + + Main->>Host: RunAsync(args, ct, stdout, stderr) + Host->>Host: intercept --help|-h|help + Host->>Parser: Parse(args) + Parser-->>Host: ParseResult + + alt parse failed + Host-->>Main: write stderr, exit 2 + else root --help / --version / --opencli + Host->>Writer: write root output + Host-->>Main: exit 0 + else command dispatch + Host->>Registry: TryResolve(alias) + Registry-->>Host: command or null + Host->>Parser: Parse(args, command) + Parser-->>Host: validated CommandRunOptions + Host->>Host: validate positional args and --initial + Host->>Host: create linked CTS(user token + timeout) + + alt options.Cat && command is IViewerCommand && RenderCatAsync returns result + Host->>Command: RenderCatAsync(options, stdout, ct) + Host->>Writer: ResultWriter.Write(result) + Host-->>Main: ExitCodes.FromResult(result) + else normal path + Host->>TG: ConfigurationManager.Enable(All), fallback to None on failure + Host->>TG: set Application.AppModel + Host->>TG: Application.Create(); app.Init() + Host->>Command: RunAsync(app, initial, options, ct) + Command-->>Host: CommandResult + Host->>TG: dispose IApplication + Host->>Writer: ResultWriter.Write(result) + Host-->>Main: ExitCodes.FromResult(result) + end + end +``` + +`CliHost` handles dispatch inline; there is no separate `CommandDispatcher` class. `OperationCanceledException` maps to `CommandStatus.Cancelled`. Other command or TG initialization exceptions map to `CommandStatus.Error` with code `io`. + +## 7. Exit Codes, JSON, and Type Names + +### 7.1 Exit codes + +| Constant | Value | Meaning | Mapping | +|----------|-------|---------|---------| +| `Ok` | 0 | Success | `CommandStatus.Ok` | +| `NoResult` | 1 | Successful no-result | `CommandStatus.NoResult` | +| `UsageError` | 2 | Usage or generic command error | parse errors, unknown command, output path failure, unknown error code | +| `ValidationError` | 65 | EX_DATAERR | `CommandStatus.Error` with `validation` or `input-too-large` | +| `IoError` | 74 | EX_IOERR | `CommandStatus.Error` with `io` | +| `Cancelled` | 130 | 128 + SIGINT | `CommandStatus.Cancelled` | + +### 7.2 JSON envelope + +| Status | Required fields | Omitted fields | +|--------|-----------------|----------------| +| `ok` | `schemaVersion`, `status`, optional `value` | `code`, `message` | +| `cancelled` | `schemaVersion`, `status` | `value`, `code`, `message` | +| `error` | `schemaVersion`, `status`, `code`, `message` | `value` | +| `no-result` | `schemaVersion`, `status` | `value`, `code`, `message` | + +Fields use camelCase. Null fields are omitted. Plain-text `Ok` output writes values directly; `JsonArray` writes one item per line; `JsonNode` writes JSON; errors write `error: {code}: {message}` to stderr. + +### 7.3 TypeNames mapping + +| CLR type | Wire name | +|----------|-----------| +| `string` | `string` | +| `int`, `long`, `short` and nullable forms | `int` | +| `decimal`, `double`, `float` and nullable forms | `decimal` | +| `bool` and nullable form | `bool` | +| `DateTime`, `DateOnly` and nullable forms | `date` | +| `TimeOnly` and nullable form | `time` | +| `TimeSpan` and nullable form | `duration` | +| `JsonArray` | `array` | +| `JsonObject` | `object` | +| `JsonNode` | `json` | +| `void` | `none` | +| other | `Type.Name` | + +## 8. AI Discovery Model + +### 8.1 `llms.txt` + +Consumers SHOULD ship `llms.txt` in the repo root and, when applicable, at `https:///llms.txt`. It is a short orientation document for agents: what the tool does, install instructions, quick start, key flags, and pointers to `agent-guide` and `--opencli`. The library does not generate it. + +### 8.2 `agent-guide` + +When `CliHostOptions.AgentGuide` is set, `CliHost` registers `AgentGuideCommand` under `agent-guide`. Behavior: + +| Invocation | Behavior | +|------------|----------| +| ` agent-guide` | Prints the guide text to stdout, exit 0. | +| ` agent-guide --json` | Writes JSON envelope with guide text as `value`, exit 0. | +| no configured guide | Command is absent; invoking it is unknown command, exit 2. | + +When `AgentGuideIsResource` is true, the value is an embedded resource name resolved from `CliHostOptions.ResourceAssembly ?? Assembly.GetEntryAssembly()`. Consumers should set `ResourceAssembly = typeof (Program).Assembly` for explicit, testable resolution. When false, `AgentGuide` is literal markdown content. + +### 8.3 `--opencli` + +Every host supports root `--opencli`. It emits OpenCLI JSON with: + +- `opencli: "0.1"` +- app `info.title` and `info.version` +- all registered commands in registry order +- aliases, descriptions, options, and exit codes +- metadata entries `kind` (`input`/`viewer`) and `resultType` (`TypeNames.WireName`) +- recursive framework options + +Agents should call `--opencli` once per session and cache the result. + +## 9. Project Structure + +```text +Terminal.Gui.Cli.slnx +Directory.Build.props +Directory.Build.targets +.editorconfig +CLAUDE.md +AGENTS.md +specs/ + constitution.md + terminal-gui-cli-v2.md +src/ + Terminal.Gui.Cli/ + Terminal.Gui.Cli.csproj + Abstractions/ + CommandKind.cs + CommandStatus.cs + CommandOptionDescriptor.cs + CommandResult.cs + ICliCommand.cs + ICliCommandGeneric.cs (contains ICliCommand) + IViewerCommand.cs + ICommandRegistry.cs + CommandRunOptions.cs + Registry/ + CommandRegistry.cs + Hosting/ + ArgParser.cs + CliHost.cs (dispatch is inline; no CommandDispatcher.cs) + CliHostOptions.cs + ExitCodes.cs + GlobalOptionDescriptor.cs + InputCommandRunner.cs + Commands/ + HelpCommand.cs + AgentGuideCommand.cs + Help/ + IHelpProvider.cs + MetadataHelpProvider.cs + EmbeddedMarkdownHelpProvider.cs + MarkdownRenderer.cs + Output/ + CliJsonContext.cs + JsonEnvelope.cs + OpenCliWriter.cs + ResultWriter.cs + TypeNames.cs + Security/ + TerminalEscapeSanitizer.cs + Properties/ + AssemblyInfo.cs +tests/ + Terminal.Gui.Cli.Tests/ + Terminal.Gui.Cli.IntegrationTests/ + Terminal.Gui.Cli.SmokeTests/ +examples/ + Terminal.Gui.Cli.ExampleApp/ +``` + +## 10. Test Strategy + +Port `tests/Terminal.Gui.Cli.Tests` from clet PR #176 as the baseline public API contract. These tests validate parser, registry, JSON envelope, exit codes, type names, sanitizer, host dispatch, help interception, OpenCLI, and consumer global option flow. + +| Tier | Project | Scope | Notes | +|------|---------|-------|-------| +| Unit | `Terminal.Gui.Cli.Tests` | Parser, registry, result writer, JSON, OpenCLI, sanitizer, type names, host paths not requiring real TUI interaction | No Terminal.Gui driver initialization except where unavoidable. | +| Integration | `Terminal.Gui.Cli.IntegrationTests` | `CliHost` end-to-end with `Application.Create()`, cancellation, timeout, `InputCommandRunner`, interactive help rendering | Full parallel; no process-global mutation unless isolated by collection. | +| Smoke | `Terminal.Gui.Cli.SmokeTests` | Spawn `examples/Terminal.Gui.Cli.ExampleApp`; verify `--help`, `--version`, `--opencli`, `agent-guide`, JSON output, exit codes | OS matrix, Release build. | + +Write fresh tests for `HelpCommand`, `AgentGuideCommand`, `ReplaceBuiltInCommand`, `CliHostOptions.ResourceAssembly`, `EmbeddedMarkdownHelpProvider`, `MarkdownRenderer`, and smoke tests tied to the new repo/example app. Tests run with `dotnet run --project tests/`. + +## 11. CI/CD + +Clone from `gui-cs/Editor` and adapt names. + +| Workflow | Trigger | Responsibilities | +|----------|---------|------------------| +| `ci.yml` | PRs and branch pushes | Restore, build Debug/Release as appropriate, format verification, unit/integration/smoke tests on ubuntu/macos/windows, AOT publish example app. | +| `release.yml` | `develop` push and `v*` tags | Compute version, Release build/test, pack, NuGet push with skip-duplicate, GitHub release artifacts for tags. | +| `prepare-release.yml` | manual dispatch | Compute next beta/rc/stable version, create release branch, open release PR to `main`. | +| `finalize-release.yml` | release PR merged | Create annotated tag/GitHub Release, delete release branch, open main-to-develop back-merge PR. | + +Versioning follows Editor: base `` in `Directory.Build.props`; develop builds append GitHub run number to a `-develop` base; tag builds strip leading `v`; `` pins the Terminal.Gui dependency and can be overridden in CI. + +## 12. Scope Boundaries + +| Out of scope | Reason | +|--------------|--------| +| Concrete consumer commands such as clet's `select`, `md`, `edit`, or config tools | Consumers own domain commands. | +| FileAccessPolicy / file access settings | clet-specific AI-agent threat model. | +| MarkdownContentResolver | clet-specific file and stdin resolution. | +| InputCletRunner | clet-specific styling wrapper around `InputCommandRunner`. | +| CletStyling / scheme names | App-specific visual policy. | +| General-purpose markdown file browser command | Library provides help/agent guide primitives, not file browsing. | +| DI container | Consumers construct commands however they want. | +| Reflection/source-generated command discovery | Explicit registration is the contract. | +| Logging abstraction | Consumers may add logging around their commands. | +| Environment-variable option fallback or short-option bundling | Not required for proven API; keep parser small. | +| Owning consumer config files | Consumers set `ConfigurationManager.AppName`; library only enables/falls back during dispatch. | + +## 13. Terminal.Gui Dependency Floor + +| Dependency | Required by | Minimum version | Requirement | +|------------|-------------|-----------------|-------------| +| `Terminal.Gui.Views.Markdown` View | `HelpCommand` interactive mode | `2.4.1-develop.11` or later | Display markdown help in a fullscreen Terminal.Gui viewer. | +| `Terminal.Gui.Views.Markdown.RenderToAnsi()` | `MarkdownRenderer`, `HelpCommand --cat` | `2.4.1-develop.11` or later | Render markdown to ANSI for stdout output before sanitizer pass-through. | + +`Directory.Build.props` must pin `TerminalGuiVersion` to a version that contains both APIs. CI should fail fast if the pinned package no longer exposes them.