This document defines how every txc command communicates results, diagnostics, and errors. All commands must follow this contract — deviations cause build failures (via BannedApiAnalyzers) or test failures (via CommandConventionTests).
| Stream | Purpose | Who writes |
|---|---|---|
| stdout | Command result data (the "answer") | OutputFormatter / OutputWriter |
| stderr | Diagnostic messages: logs, progress, warnings, errors | ILogger (via TxcLoggerFactory) |
| exit code | Machine-readable success/failure signal | Return value of ExecuteAsync() |
Rule: Never write diagnostic/log messages to stdout. Never write result data to stderr.
Every leaf command inherits a --format / -f option from TxcLeafCommand:
| Value | Behavior |
|---|---|
json |
JSON output via TxcOutputJsonOptions.Default (camelCase, indented, null-safe) |
text |
Human-friendly tables, key-value pairs, or plain strings |
| (omitted) | TTY auto-detection: text when stdout is a terminal, JSON when piped or redirected |
# Interactive terminal → text table
txc env entity list
# Piped → JSON automatically
txc env entity list | jq '.[] | .logicalName'
# Explicit override
txc env entity list --format json
txc env entity list --format text| Code | Meaning | When to use |
|---|---|---|
0 (ExitSuccess) |
Operation completed successfully | Default success path |
1 (ExitError) |
Runtime/operational error | Service call failed, network error, unexpected exception |
2 (ExitValidationError) |
Input validation error or resource not found | Bad arguments, missing required input, entity not found |
The base class TxcLeafCommand.RunAsync() catches unhandled exceptions and returns ExitError (1) automatically. Commands only need explicit exit code handling for validation errors (2).
Every leaf command must extend TxcLeafCommand (or ProfiledCliCommand for environment-facing commands):
[CliCommand(Name = "list", Description = "List widgets.")]
public class WidgetListCliCommand : ProfiledCliCommand
{
protected override ILogger Logger { get; } = TxcLoggerFactory.CreateLogger(nameof(WidgetListCliCommand));
[CliOption(Name = "--search", Description = "Filter by name.", Required = false)]
public string? Search { get; set; }
protected override async Task<int> ExecuteAsync()
{
var service = TxcServices.Get<IWidgetService>();
var widgets = await service.ListAsync(Profile, Search);
OutputFormatter.WriteList(widgets, PrintTable);
return ExitSuccess;
}
private static void PrintTable(IReadOnlyList<Widget> items)
{
// Text table rendering — only called in text mode
foreach (var w in items)
OutputWriter.WriteLine($" {w.Name,-30} {w.Status}");
}
}--format/-foption — inherited by all leaf commandsOutputContextsetup — applies the format flag with TTY auto-detection- Standardized try/catch — catches
ConfigurationResolutionException,OperationCanceledException, and general exceptions ILoggerrequirement —protected abstract ILogger Loggerensures every command has logging- Exit code constants —
ExitSuccess(0),ExitError(1),ExitValidationError(2)
- ❌ Define their own
RunAsync()— the base class owns it - ❌ Use
Console.Write*orConsole.ReadKey— useOutputWriter/OutputFormatterandIHeadlessDetector - ❌ Create local
JsonSerializerOptions— useTxcOutputJsonOptions.Default - ❌ Add
--jsonflags — use the inherited--formatflag instead - ❌ Catch
ConfigurationResolutionException— the base class handles it
| API | When to use |
|---|---|
OutputFormatter.WriteData<T>(data, textRenderer?) |
Single object output |
OutputFormatter.WriteList<T>(items, tableRenderer?) |
Collection output |
OutputFormatter.WriteResult(status, message?, id?) |
Mutative command result envelope |
OutputFormatter.WriteValue(key, value) |
Single scalar value |
OutputFormatter.WriteDynamicTable(records, tableRenderer) |
Dynamic-schema query results |
OutputFormatter.WriteRaw(json, textRenderer?) |
Pre-serialized JSON passthrough |
Commands that create, update, or delete resources return a standardized envelope:
{
"status": "succeeded",
"message": "Record created successfully.",
"id": "a1b2c3d4-..."
}In text mode, only the human-readable message is printed.
- Use
ILogger(viaTxcLoggerFactory.CreateLogger(nameof(X))) for all diagnostic output - Logs go to stderr in all modes:
- Terminal mode:
TxcConsoleFormatter—[INFO]/[WARN]/[ERROR]prefixes with ANSI color,HH:mm:sstimestamps, no category names. Stack traces are suppressed by default; shown only atDebuglevel or with--verbose. - Pipe/MCP mode: structured JSON lines to stderr (
TXC_LOG_FORMAT=json) — always includes full exception details.
- Terminal mode:
- Log level controlled by
TXC_LOG_LEVELenv var (default:Information)
The MCP server (txc-mcp) spawns txc as a subprocess with TXC_LOG_FORMAT=json and TXC_NON_INTERACTIVE=1:
- stdout → captured as the MCP tool result (JSON by default since stdout is redirected)
- stderr → parsed as JSON log lines, forwarded as MCP log notifications
- exit code → determines
isErrorin the MCP tool result
Since commands default to JSON when stdout is redirected, the MCP server gets structured data automatically — no --json injection needed.
| Mechanism | What it catches | When |
|---|---|---|
BannedApiAnalyzers (RS0030) |
Console.Write*, Console.ReadKey, new HttpClient(), new JsonSerializerOptions(), Thread.Sleep, Task.Result/.GetAwaiter().GetResult(), throw new Exception(), Newtonsoft.Json |
Build time (error) |
TxcLeafCommand abstract members |
Missing Logger, missing ExecuteAsync() |
Build time (error) |
TXC001 (Roslyn analyzer) |
Leaf [CliCommand] not inheriting TxcLeafCommand |
Build time (error) |
TXC002 (Roslyn analyzer) |
Leaf command defining own RunAsync() |
Build time (error) |
TXC003 (Roslyn analyzer) |
Direct OutputWriter calls in command code (auto-suppresses text-renderer lambdas) |
Build time (error) |
CommandConventionTests |
Non-conforming commands, stale --json flags, local JsonSerializerOptions |
Test time |
LayeringTests |
Feature→Feature project references, --yes commands missing [McpIgnore] |
Test time |
- Create a class with
[CliCommand]extendingTxcLeafCommand(orProfiledCliCommand) - Implement
protected override ILogger Logger { get; }andprotected override Task<int> ExecuteAsync() - Use
OutputFormatterfor all output - Return
ExitSuccess,ExitError, orExitValidationError - Run tests to verify convention compliance