You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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
The library owns Terminal.Gui lifecycle. Commands receive an initialized IApplication; commands never call Application.Create(), app.Init(), or Application.Init().
No reflection; NativeAOT from day one. Commands self-describe via properties. JSON serialization uses source generation. Registration is explicit.
Registry stores instances. Commands are constructed by the consumer and registered as instances, supporting DI, conditional registration, and plugins.
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.
JSON envelope is the stable wire contract.{ schemaVersion, status, value?, code?, message? } is controlled by the library and append-only within schema v1.
Exit codes are library-controlled. Commands return CommandResult; the library maps status/error code to POSIX-conventional process codes.
AI discoverability is first-class.llms.txt, agent-guide, and --opencli are normative parts of the model.
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<T> live together in CommandResult.cs, and ICliCommand<TValue> 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
namespaceTerminal.Gui.Cli;/// <summary>The two kinds of CLI commands the library knows about.</summary>publicenumCommandKind{/// <summary>An interactive command that returns a typed value.</summary>Input,/// <summary>An interactive or headless command that does not return a typed result value.</summary>Viewer}/// <summary>Outcome status of a command run.</summary>publicenumCommandStatus{/// <summary>The command completed successfully.</summary>Ok,/// <summary>The user or caller cancelled the command.</summary>Cancelled,/// <summary>The command failed.</summary>Error,/// <summary>The command completed but produced no result.</summary>NoResult}/// <summary>Metadata descriptor for a per-command option.</summary>publicsealedrecordCommandOptionDescriptor(stringName,string?ShortName,TypeValueType,stringDescription,boolRequired,string?DefaultValue);/// <summary>Non-generic result for dispatch and output formatting.</summary>publicreadonlyrecordstructCommandResult(CommandStatusStatus,object?Value,string?ErrorCode,string?ErrorMessage);/// <summary>Typed result returned by input commands.</summary>publicreadonlyrecordstructCommandResult<T>(CommandStatusStatus,T?Value,string?ErrorCode,string?ErrorMessage);
CommandResult and CommandResult<T> must remain readonly record structs in one file because ICliCommand<TValue> 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
usingTerminal.Gui.App;namespaceTerminal.Gui.Cli;/// <summary>A CLI command backed by Terminal.Gui. Implemented by consumer apps and built-ins.</summary>publicinterfaceICliCommand{/// <summary>The canonical alias shown in help and OpenCLI output.</summary>stringPrimaryAlias{get;}/// <summary>All aliases that resolve to this command. Must include <see cref="PrimaryAlias" />.</summary>IReadOnlyList<string>Aliases{get;}/// <summary>Human-readable one-line command description.</summary>stringDescription{get;}/// <summary>The command kind.</summary>CommandKindKind{get;}/// <summary>The CLR type of the value written to the JSON envelope, or <see cref="void" />.</summary>TypeResultType{get;}/// <summary>Per-command options accepted by this command.</summary>IReadOnlyList<CommandOptionDescriptor>Options{get;}/// <summary>Whether this command consumes positional arguments.</summary>boolAcceptsPositionalArgs=>false;/// <summary>/// Validates the --initial value before Terminal.Gui starts. The default permits any value;/// commands override this method when they need command-specific validation./// </summary>boolTryValidateInitial(stringinitial,CommandRunOptionsoptions)=>true;/// <summary>Runs the command after the host has initialized Terminal.Gui.</summary>Task<CommandResult>RunAsync(IApplicationapp,string?initial,CommandRunOptionsoptions,CancellationTokencancellationToken);}/// <summary>Typed command that returns a value.</summary>publicinterfaceICliCommand<TValue>:ICliCommand{/// <summary>Runs the command and returns a typed result.</summary>newTask<CommandResult<TValue>>RunAsync(IApplicationapp,string?initial,CommandRunOptionsoptions,CancellationTokencancellationToken);asyncTask<CommandResult>ICliCommand.RunAsync(IApplicationapp,string?initial,CommandRunOptionsoptions,CancellationTokencancellationToken){CommandResult<TValue>result=awaitRunAsync(app,initial,options,cancellationToken);returnnew(result.Status,result.Value,result.ErrorCode,result.ErrorMessage);}}/// <summary>/// 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./// </summary>publicinterfaceIViewerCommand:ICliCommand{/// <summary>/// 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./// </summary>Task<CommandResult?>RenderCatAsync(CommandRunOptionsoptions,TextWriterstdout,CancellationTokencancellationToken)=>Task.FromResult<CommandResult?>(null);}
4.3 Registry
namespaceTerminal.Gui.Cli;/// <summary>Manages alias-to-command lookup.</summary>publicinterfaceICommandRegistry{/// <summary>Registers a command instance.</summary>/// <exception cref="InvalidOperationException">/// Thrown when <c>PrimaryAlias</c> is not present in <c>Aliases</c>, or any alias is already registered./// </exception>voidRegister(ICliCommandcommand);/// <summary>Resolves an alias case-insensitively.</summary>boolTryResolve(stringalias,outICliCommand?command);/// <summary>All registered commands in registration order.</summary>IReadOnlyCollection<ICliCommand>All{get;}}/// <summary>Default case-insensitive, duplicate-rejecting command registry.</summary>publicsealedclassCommandRegistry:ICommandRegistry{/// <inheritdoc />publicvoidRegister(ICliCommandcommand);/// <inheritdoc />publicboolTryResolve(stringalias,outICliCommand?command);/// <inheritdoc />publicIReadOnlyCollection<ICliCommand>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
namespaceTerminal.Gui.Cli;/// <summary>Parsed options bag passed to commands.</summary>publicsealedclassCommandRunOptions{/// <summary>Pre-fill value for the View.</summary>publicstring?Initial{get;init;}/// <summary>Title override for TUI chrome. --prompt/-p is an alias for --title/-t.</summary>publicstring?Title{get;init;}/// <summary>Whether to emit the JSON envelope instead of plain text.</summary>publicboolJsonOutput{get;init;}/// <summary>Cancel after this duration.</summary>publicTimeSpan?Timeout{get;init;}/// <summary>Force fullscreen. Input commands otherwise default to inline.</summary>publicboolFullscreen{get;init;}/// <summary>Render supported viewer content to stdout instead of launching the TUI.</summary>publicboolCat{get;init;}/// <summary>Write successful command output to this file instead of stdout.</summary>publicstring?OutputPath{get;init;}/// <summary>Constrain inline height.</summary>publicint?Rows{get;init;}/// <summary>Positional arguments after the alias.</summary>publicIReadOnlyList<string>Arguments{get;init;}=[];/// <summary>Per-command option values keyed by long option name without dashes.</summary>publicIReadOnlyDictionary<string,string>CommandOptions{get;init;}=newDictionary<string,string>();/// <summary>Consumer-registered global option values keyed by long option name without dashes.</summary>publicIReadOnlyDictionary<string,IReadOnlyList<string>>Extensions{get;init;}=newDictionary<string,IReadOnlyList<string>>();/// <summary>Gets the last value for a single-value consumer extension, parsed by <paramref name="parser" />.</summary>publicT?GetExtension<T>(stringkey,Func<string,T>parser,T?defaultValue=default);/// <summary>Gets all values for a repeatable consumer extension.</summary>publicIReadOnlyList<string>GetExtensionList(stringkey);/// <summary>Returns true when a consumer extension flag or value is present.</summary>publicboolHasExtension(stringkey);}/// <summary>Describes a consumer-defined global option.</summary>publicsealedrecordGlobalOptionDescriptor(stringName,string?ShortName,stringDescription,boolIsFlag,boolRepeatable=false);
4.5 Host
usingSystem.Reflection;namespaceTerminal.Gui.Cli;/// <summary>The main entry point. Owns parsing, dispatch, Terminal.Gui lifecycle, and output.</summary>publicsealedclassCliHost{/// <summary>Creates a host, applies configuration, creates its registry, and registers built-ins.</summary>publicCliHost(Action<CliHostOptions>?configure=null);/// <summary>The command registry owned by this host. Register consumer commands before RunAsync.</summary>publicICommandRegistryRegistry{get;}/// <summary>Parses args, dispatches a command, writes output, and returns a process exit code.</summary>publicTask<int>RunAsync(string[]args,CancellationTokencancellationToken=default,TextWriter?stdout=null,TextWriter?stderr=null);}/// <summary>Configuration options for <see cref="CliHost" />.</summary>publicsealedclassCliHostOptions{/// <summary>Application name shown in help, version output, and OpenCLI.</summary>publicstringApplicationName{get;set;}="app";/// <summary>Version string shown in --version and OpenCLI. Null uses 0.0.0.</summary>publicstring?Version{get;set;}/// <summary>Custom help provider. Null uses <see cref="MetadataHelpProvider" />.</summary>publicIHelpProvider?HelpProvider{get;set;}/// <summary>Maximum characters accepted by --initial. Default is 64 KiB.</summary>publicintMaxInitialChars{get;set;}=64*1024;/// <summary>Agent guide embedded resource name or literal markdown. Null disables agent-guide.</summary>publicstring?AgentGuide{get;set;}/// <summary>True when <see cref="AgentGuide" /> is an embedded resource name; false when literal content.</summary>publicboolAgentGuideIsResource{get;set;}=true;/// <summary>Assembly used to resolve embedded resources. Null falls back to <see cref="Assembly.GetEntryAssembly" />.</summary>publicAssembly?ResourceAssembly{get;set;}/// <summary>Consumer-defined global options parsed into <see cref="CommandRunOptions.Extensions" />.</summary>publicList<GlobalOptionDescriptor>GlobalOptions{get;}=[];/// <summary>Replaces a library built-in command before it is registered.</summary>/// <exception cref="ArgumentException">Thrown when <paramref name="alias" /> is not a replaceable built-in alias.</exception>/// <exception cref="InvalidOperationException">Thrown when the same built-in alias is replaced more than once.</exception>publicvoidReplaceBuiltInCommand(stringalias,ICliCommandreplacement);}
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.
namespaceTerminal.Gui.Cli;/// <summary>Data-driven parser for framework flags, consumer globals, and per-command options.</summary>publicsealedclassArgParser{/// <summary>Creates a parser with registered consumer globals and an --initial limit.</summary>publicArgParser(List<GlobalOptionDescriptor>globalOptions,intmaxInitialChars=64*1024);/// <summary>Parses command-line arguments, optionally validating against a resolved command.</summary>publicParseResultParse(string[]args,ICliCommand?command=null);/// <summary>Parses duration strings accepted by --timeout: ms, s, m, h.</summary>publicstaticboolTryParseTimeout(stringinput,outTimeSpantimeout);/// <summary>Represents the result of parsing arguments.</summary>publicsealedclassParseResult{/// <summary>True if parsing succeeded.</summary>publicboolSuccess{get;init;}/// <summary>Error message when parsing failed.</summary>publicstring?Error{get;init;}/// <summary>The command alias, when this is not a root flag.</summary>publicstring?Alias{get;init;}/// <summary>The parsed initial value.</summary>publicstring?Initial{get;init;}/// <summary>The parsed options bag.</summary>publicCommandRunOptions?Options{get;init;}/// <summary>Root flag detected before command dispatch.</summary>publicRootFlag?RootFlag{get;init;}/// <summary>Creates a failed parse result.</summary>publicstaticParseResultFail(stringerror);}/// <summary>Root flags that exit without command dispatch.</summary>publicenumRootFlag{/// <summary>Root --help or -h.</summary>Help,/// <summary>Root --version.</summary>Version,/// <summary>Root --opencli.</summary>OpenCli}}
4.7 Help and built-in commands
usingSystem.Reflection;usingTerminal.Gui.App;namespaceTerminal.Gui.Cli;/// <summary>Pluggable help rendering.</summary>publicinterfaceIHelpProvider{/// <summary>Renders root-level help. Return null to use generated fallback text.</summary>string?GetRootHelp(ICommandRegistryregistry);/// <summary>Renders per-command help. Return null to use generated fallback text.</summary>string?GetCommandHelp(ICliCommandcommand);}/// <summary>Generates help text from registry metadata.</summary>publicsealedclassMetadataHelpProvider:IHelpProvider{/// <inheritdoc />publicstring?GetRootHelp(ICommandRegistryregistry);/// <inheritdoc />publicstring?GetCommandHelp(ICliCommandcommand);}/// <summary>Reads embedded markdown resources for root, command, and agent help.</summary>publicsealedclassEmbeddedMarkdownHelpProvider:IHelpProvider{/// <summary>Creates a provider that reads markdown resources from <paramref name="resourceAssembly" />.</summary>publicEmbeddedMarkdownHelpProvider(AssemblyresourceAssembly);/// <inheritdoc />publicstring?GetRootHelp(ICommandRegistryregistry);/// <inheritdoc />publicstring?GetCommandHelp(ICliCommandcommand);/// <summary>Reads an embedded markdown resource by exact manifest resource name.</summary>publicstring?GetMarkdownResource(stringresourceName);}/// <summary>Interactive TUI markdown help viewer, with --cat support for ANSI stdout.</summary>publicsealedclassHelpCommand:IViewerCommand{/// <summary>Creates a help command that lazily reads command metadata from <paramref name="registry" />.</summary>publicHelpCommand(ICommandRegistryregistry,IHelpProviderhelpProvider);/// <inheritdoc />publicstringPrimaryAlias{get;}/// <inheritdoc />publicIReadOnlyList<string>Aliases{get;}/// <inheritdoc />publicstringDescription{get;}/// <inheritdoc />publicCommandKindKind{get;}/// <inheritdoc />publicTypeResultType{get;}/// <inheritdoc />publicIReadOnlyList<CommandOptionDescriptor>Options{get;}/// <inheritdoc />publicboolAcceptsPositionalArgs{get;}/// <inheritdoc />publicTask<CommandResult>RunAsync(IApplicationapp,string?initial,CommandRunOptionsoptions,CancellationTokencancellationToken);/// <inheritdoc />publicTask<CommandResult?>RenderCatAsync(CommandRunOptionsoptions,TextWriterstdout,CancellationTokencancellationToken);}/// <summary>Non-interactive viewer command that prints the consumer's agent guide.</summary>publicsealedclassAgentGuideCommand:IViewerCommand{/// <summary>Creates an agent guide command from resolved markdown content.</summary>publicAgentGuideCommand(stringmarkdown);/// <inheritdoc />publicstringPrimaryAlias{get;}/// <inheritdoc />publicIReadOnlyList<string>Aliases{get;}/// <inheritdoc />publicstringDescription{get;}/// <inheritdoc />publicCommandKindKind{get;}/// <inheritdoc />publicTypeResultType{get;}/// <inheritdoc />publicIReadOnlyList<CommandOptionDescriptor>Options{get;}/// <inheritdoc />publicTask<CommandResult>RunAsync(IApplicationapp,string?initial,CommandRunOptionsoptions,CancellationTokencancellationToken);/// <inheritdoc />publicTask<CommandResult?>RenderCatAsync(CommandRunOptionsoptions,TextWriterstdout,CancellationTokencancellationToken);}
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
namespaceTerminal.Gui.Cli;/// <summary>The stable wire format for CLI output.</summary>publicsealedclassJsonEnvelope{/// <summary>Wire schema version. Always 1 for library major version 1.x.</summary>publicintSchemaVersion{get;init;}=1;/// <summary>Status string: ok, cancelled, error, or no-result.</summary>publicstringStatus{get;init;}="ok";/// <summary>Result value. Omitted when null.</summary>publicobject?Value{get;init;}/// <summary>Error code. Omitted when null.</summary>publicstring?Code{get;init;}/// <summary>Error message. Omitted when null.</summary>publicstring?Message{get;init;}/// <summary>Creates an ok envelope.</summary>publicstaticJsonEnvelopeOk(object?value=null);/// <summary>Creates a cancelled envelope.</summary>publicstaticJsonEnvelopeCancelled();/// <summary>Creates an error envelope.</summary>publicstaticJsonEnvelopeError(stringcode,stringmessage);/// <summary>Creates a no-result envelope.</summary>publicstaticJsonEnvelopeNoResult();/// <summary>Serializes using the source-generated JSON context.</summary>publicstringToJson();}/// <summary>Formats command results to stdout, stderr, or an output file.</summary>publicstaticclassResultWriter{/// <summary>Writes <paramref name="result" /> and returns false when output file creation fails.</summary>publicstaticboolWrite(CommandResultresult,booljsonOutput,TextWriterstdout,TextWriterstderr,string?outputPath=null);}/// <summary>Generates an OpenCLI JSON document from registry metadata.</summary>publicstaticclassOpenCliWriter{/// <summary>Generates OpenCLI JSON for the registered commands and framework options.</summary>publicstaticstringGenerate(ICommandRegistryregistry,CliHostOptionsoptions);}
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:
namespaceTerminal.Gui.Cli;/// <summary>POSIX-conventional exit codes.</summary>publicstaticclassExitCodes{/// <summary>Success.</summary>publicconstintOk=0;/// <summary>Successful command execution with no result.</summary>publicconstintNoResult=1;/// <summary>Usage error: bad command, bad option, or output-file creation failure.</summary>publicconstintUsageError=2;/// <summary>Validation error, equivalent to sysexits EX_DATAERR.</summary>publicconstintValidationError=65;/// <summary>I/O error, equivalent to sysexits EX_IOERR.</summary>publicconstintIoError=74;/// <summary>Cancelled, equivalent to 128 + SIGINT.</summary>publicconstintCancelled=130;/// <summary>Maps a command result to a process exit code.</summary>publicstaticintFromResult(CommandResultresult);}/// <summary>Maps CLR types to stable wire-format type names.</summary>publicstaticclassTypeNames{/// <summary>Returns the wire-format name for <paramref name="type" />.</summary>publicstaticstringWireName(Typetype);}/// <summary>Strips dangerous terminal escape sequences from untrusted content.</summary>publicstaticclassTerminalEscapeSanitizer{/// <summary>Sanitizes user-supplied content before it reaches a terminal driver.</summary>publicstaticstring?Sanitize(string?input);/// <summary>Sanitizes rendered ANSI, preserving only SGR CSI sequences generated by trusted renderers.</summary>publicstaticstringSanitizeRenderedOutput(stringrenderedAnsi);}/// <summary>Markdown-to-ANSI helper for help and viewer output.</summary>publicstaticclassMarkdownRenderer{/// <summary>Renders markdown as ANSI to <paramref name="output" /> and sanitizes rendered output.</summary>publicstaticvoidRenderToAnsi(stringmarkdown,TextWriteroutput);}
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
usingTerminal.Gui.App;usingTerminal.Gui.ViewBase;usingTerminal.Gui.Views;namespaceTerminal.Gui.Cli;/// <summary>Shared boilerplate for input commands that wrap a control in RunnableWrapper.</summary>publicstaticclassInputCommandRunner{/// <summary>Configures, runs, and maps the result from an input command wrapper when raw result and output value differ.</summary>publicstaticTask<CommandResult<TValue>>RunAsync<TControl,TRawResult,TValue>(IApplicationapp,RunnableWrapper<TControl,TRawResult>wrapper,CommandRunOptionsoptions,stringdefaultTitle,CancellationTokencancellationToken,Func<TRawResult?,CommandResult<TValue>>resultMapper,booladdEnterBinding=true)whereTControl:View,new();/// <summary>Configures and runs a wrapper whose raw result is already the output value.</summary>publicstaticTask<CommandResult<TValue>>RunAsync<TControl,TValue>(IApplicationapp,RunnableWrapper<TControl,TValue>wrapper,CommandRunOptionsoptions,stringdefaultTitle,CancellationTokencancellationToken,booladdEnterBinding=true)whereTControl: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.
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
<alias> --help, <alias> -h, and <alias> 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
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 <alias> --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
Loading
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.
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://<domain>/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
<app> agent-guide
Prints the guide text to stdout, exit 0.
<app> 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.
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.
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/<project>.
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 <Version> in Directory.Build.props; develop builds append GitHub run number to a -develop base; tag builds strip leading v; <TerminalGuiVersion> 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.
Terminal.Gui.Cli Library Specification
0. Repo Identity
gui-cs/Terminal.Gui.cliis a new gui-cs library repo for the NuGet packageTerminal.Gui.Cli. This repo/package casing split is normative: the repo slug uses lowercasecli; the package, assembly, and namespace use theTerminal.Gui.Cliidentifier with the final segment spelledCli. Before writing library code, scaffold the repo by cloning the structure and maintenance model ofgui-cs/Editor(the Terminal.Gui.Editor repo):developis the integration branch;mainis the stable release branch.specs/constitution.md; it is the highest-authority engineering document.CLAUDE.md,AGENTS.md,.editorconfig,Directory.Build.props,Directory.Build.targets, solution naming, and test conventions.ci.yml,release.yml,prepare-release.yml, andfinalize-release.yml.Repository/package identity:
gui-cs/Terminal.Gui.cliTerminal.Gui.CliTerminal.Gui.Clinet10.0Terminal.Guionly<IsAotCompatible>true</IsAotCompatible>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.
cletproved this pattern with 18 commands and 500+ tests, but the infrastructure should be a reusable library rather than app-specific hosting code.Terminal.Gui.Clisupplies that reusable layer. A consumer creates aCliHost, registers command instances, and callsRunAsync. 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
IApplication; commands never callApplication.Create(),app.Init(), orApplication.Init().ArgParser; consumer globals are declared inCliHostOptions.GlobalOptionsand land inCommandRunOptions.Extensions; per-command options are declared byCommandOptionDescriptorand land inCommandRunOptions.CommandOptions.{ schemaVersion, status, value?, code?, message? }is controlled by the library and append-only within schema v1.CommandResult; the library maps status/error code to POSIX-conventional process codes.llms.txt,agent-guide, and--opencliare normative parts of the model.3. Engineering Constitution
specs/constitution.md, cloned and adapted fromgui-cs/Editor, is canonical. It must include these Terminal.Gui.Cli-specific rules:CliHostcalls Terminal.Gui lifecycle APIs and disposesIApplication.Environment.Exit; returnCommandResult.The constitution must also document two narrow file-layout exceptions proven by the clet prototype in PR #176:
CommandResultandCommandResult<T>live together inCommandResult.cs, andICliCommand<TValue>lives inICliCommandGeneric.cs. Do not use angle brackets in filenames:<and>are invalid on Windows and awkward in POSIX shells. TheGenericsuffix 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
CommandResultandCommandResult<T>must remain readonly record structs in one file becauseICliCommand<TValue>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
4.3 Registry
PrimaryAliasmatching is case-insensitive. Duplicate alias detection is case-insensitive. Registration failure throwsInvalidOperationException.4.4 Run options and global option descriptors
4.5 Host
CliHostconstructs and owns itsCommandRegistry. It registers built-ins during construction after applying options:helpis always registered unless replaced;agent-guideis registered only whenAgentGuideis non-null unless replaced. WhenAgentGuideIsResourceis true,CliHostresolvesAgentGuidefromResourceAssembly ?? Assembly.GetEntryAssembly()during construction and passes the resolved markdown string toAgentGuideCommand; missing assembly or missing resource throwsInvalidOperationException. WhenAgentGuideIsResourceis false,AgentGuideis already literal markdown.ReplaceBuiltInCommandsupports reserved aliaseshelpandagent-guide; a replacement forhelpmust includehelpinAliases, and a replacement foragent-guidemust includeagent-guideinAliases.4.6 Parser
ArgParseris 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.4.7 Help and built-in commands
HelpCommandusesMarkdownRendererfor ANSI output andTerminal.Gui.Views.Markdownfor interactive TUI mode.AgentGuideCommandis headless: it returns the guide markdown as the command value so plain output prints the text and--jsonwraps it in the envelope.4.8 Output and JSON
OpenCliWriterhand-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:
4.9 Utilities
MarkdownRendererwrapsTerminal.Gui.Views.Markdown.RenderToAnsi()and then appliesTerminalEscapeSanitizer.SanitizeRenderedOutputbefore writing to the targetTextWriter. The required Terminal.Gui version is listed in the Terminal.Gui Dependency Floor section.4.10 InputCommandRunner
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
TRawResultandTValueare the same and the wrapper result can be returned directly.InputCommandRunnerapplies these defaults beforeapp.RunAsync:Title = options.Title ?? defaultTitle,Width = Dim.Fill(),BorderStyle = LineStyle.Rounded,Border.Thickness = new Thickness (0, 1, 0, 0), and Enter key binding toCommand.Acceptwhen requested. Consumers override using standard Terminal.Gui lifecycle, especiallywrapper.Initialized; the library must not add custom styling callbacks.5. CLI Grammar
There is no
listcommand. Human listing is--help; structured listing is--opencli.5.1 Framework command flags
--json-jJsonOutput=true--initial-iInitial--title-tTitle--prompt-pTitle--title/-t.--timeoutTimeoutms,s,m, orh.--fullscreen-fFullscreen=true--catCat=true--output-oOutputPath--rows-rRows5.2 Root-only framework flags
These are intercepted before command dispatch and do not populate
CommandRunOptions.--help-h--version--opencliSupported 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
GlobalOptionDescriptorvalues toCliHostOptions.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
OptionsbyNameorShortName. 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 toValueType.5.5 Help interception
<alias> --help,<alias> -h, and<alias> helpare intercepted byCliHost.RunAsyncbeforeArgParser.Parse(). This is required because--helpis 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
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 <alias> --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 endCliHosthandles dispatch inline; there is no separateCommandDispatcherclass.OperationCanceledExceptionmaps toCommandStatus.Cancelled. Other command or TG initialization exceptions map toCommandStatus.Errorwith codeio.7. Exit Codes, JSON, and Type Names
7.1 Exit codes
OkCommandStatus.OkNoResultCommandStatus.NoResultUsageErrorValidationErrorCommandStatus.Errorwithvalidationorinput-too-largeIoErrorCommandStatus.ErrorwithioCancelledCommandStatus.Cancelled7.2 JSON envelope
okschemaVersion,status, optionalvaluecode,messagecancelledschemaVersion,statusvalue,code,messageerrorschemaVersion,status,code,messagevalueno-resultschemaVersion,statusvalue,code,messageFields use camelCase. Null fields are omitted. Plain-text
Okoutput writes values directly;JsonArraywrites one item per line;JsonNodewrites JSON; errors writeerror: {code}: {message}to stderr.7.3 TypeNames mapping
stringstringint,long,shortand nullable formsintdecimal,double,floatand nullable formsdecimalbooland nullable formboolDateTime,DateOnlyand nullable formsdateTimeOnlyand nullable formtimeTimeSpanand nullable formdurationJsonArrayarrayJsonObjectobjectJsonNodejsonvoidnoneType.Name8. AI Discovery Model
8.1
llms.txtConsumers SHOULD ship
llms.txtin the repo root and, when applicable, athttps://<domain>/llms.txt. It is a short orientation document for agents: what the tool does, install instructions, quick start, key flags, and pointers toagent-guideand--opencli. The library does not generate it.8.2
agent-guideWhen
CliHostOptions.AgentGuideis set,CliHostregistersAgentGuideCommandunderagent-guide. Behavior:<app> agent-guide<app> agent-guide --jsonvalue, exit 0.When
AgentGuideIsResourceis true, the value is an embedded resource name resolved fromCliHostOptions.ResourceAssembly ?? Assembly.GetEntryAssembly(). Consumers should setResourceAssembly = typeof (Program).Assemblyfor explicit, testable resolution. When false,AgentGuideis literal markdown content.8.3
--opencliEvery host supports root
--opencli. It emits OpenCLI JSON with:opencli: "0.1"info.titleandinfo.versionkind(input/viewer) andresultType(TypeNames.WireName)Agents should call
--openclionce per session and cache the result.9. Project Structure
10. Test Strategy
Port
tests/Terminal.Gui.Cli.Testsfrom 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.Terminal.Gui.Cli.TestsTerminal.Gui.Cli.IntegrationTestsCliHostend-to-end withApplication.Create(), cancellation, timeout,InputCommandRunner, interactive help renderingTerminal.Gui.Cli.SmokeTestsexamples/Terminal.Gui.Cli.ExampleApp; verify--help,--version,--opencli,agent-guide, JSON output, exit codesWrite fresh tests for
HelpCommand,AgentGuideCommand,ReplaceBuiltInCommand,CliHostOptions.ResourceAssembly,EmbeddedMarkdownHelpProvider,MarkdownRenderer, and smoke tests tied to the new repo/example app. Tests run withdotnet run --project tests/<project>.11. CI/CD
Clone from
gui-cs/Editorand adapt names.ci.ymlrelease.ymldeveloppush andv*tagsprepare-release.ymlmain.finalize-release.ymlVersioning follows Editor: base
<Version>inDirectory.Build.props; develop builds append GitHub run number to a-developbase; tag builds strip leadingv;<TerminalGuiVersion>pins the Terminal.Gui dependency and can be overridden in CI.12. Scope Boundaries
select,md,edit, or config toolsInputCommandRunner.ConfigurationManager.AppName; library only enables/falls back during dispatch.13. Terminal.Gui Dependency Floor
Terminal.Gui.Views.MarkdownViewHelpCommandinteractive mode2.4.1-develop.11or laterTerminal.Gui.Views.Markdown.RenderToAnsi()MarkdownRenderer,HelpCommand --cat2.4.1-develop.11or laterDirectory.Build.propsmust pinTerminalGuiVersionto a version that contains both APIs. CI should fail fast if the pinned package no longer exposes them.