Five tools under one repo, all docs organized per DOCS-GUIDE.md: - aalogcli: .NET 4.8 / x86 CliFx CLI for reading System Platform binary logs (*.aaLGX) for LLM debugging, built on aaOpenSource/aaLog. Commands: last, tail, range, unread, fields. Stable JSON envelope under --llm-json. Build template under lib/build/ for rebuilding aaLogReader.dll. - aot: ArchestrA Object Toolkit 2014 v4.0 reference material. Dev guide (Markdown converted from CHM), API reference for the ArchestrA.Toolkit namespace, and the Monitor / Watchdog VS sample solutions. - graccesscli: .NET 4.8 / x86 CliFx CLI that automates Galaxy configuration via the ArchestrA GRAccess COM interop. Includes session daemon, IPC protocol, and llm-json envelope contract. - grdb: SQL/DDL exploration of the Galaxy Repository database. DDL captures, reusable queries, hierarchy / contained-name <-> tag-name translation notes. - histdb: LLM-oriented reference for AVEVA Historian retrieval. INSQL linked-server, extension tables, every wwXxx time-domain extension, every retrieval mode, alarm/event SQL recipes, REST API. Distilled from the 243-page Historian Retrieval Guide. Root contains: - CLAUDE.md: thin index pointing into each tool's README. - DOCS-GUIDE.md: doctrine for organizing docs for LLM consumption. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6.6 KiB
CliFx Reference (v2.3.6)
Local reference for the CliFx CLI framework. Source: https://github.com/Tyrrrz/CliFx
Setup
using CliFx;
public static class Program
{
public static async Task<int> Main() =>
await new CliApplicationBuilder()
.AddCommandsFromThisAssembly()
.Build()
.RunAsync();
}
Important: Main() must return the integer exit code from RunAsync().
RunAsync() resolves args from Environment.GetCommandLineArgs() and env vars from Environment.GetEnvironmentVariables(). You can also provide them manually via overloads.
Defining Commands
Commands implement ICommand and are decorated with [Command]:
using CliFx;
using CliFx.Attributes;
[Command(Description = "Calculates the logarithm of a value.")]
public class LogCommand : ICommand
{
[CommandParameter(0, Description = "Value whose logarithm is to be found.")]
public required double Value { get; init; }
[CommandOption("base", 'b', Description = "Logarithm base.")]
public double Base { get; init; } = 10;
public ValueTask ExecuteAsync(IConsole console)
{
var result = Math.Log(Value, Base);
console.Output.WriteLine(result);
return default;
}
}
Use IConsole (not System.Console) for all console I/O — this enables testability.
Parameters vs Options
| Aspect | [CommandParameter] |
[CommandOption] |
|---|---|---|
| Binding | By position order | By name (--foo) or short name (-f) |
| Required by default | Yes | No |
| Make optional | Omit required (last param only) |
Omit required |
| Make required | Use required keyword |
Use required keyword |
| Non-scalar (collections) | Only last parameter | Any option |
| Env var fallback | No | Yes (EnvironmentVariable = "ENV_FOO") |
// Required option
[CommandOption("foo")]
public required string Foo { get; init; }
// Optional parameter (must be last)
[CommandParameter(0)]
public string? OptionalParam { get; init; }
// Option with env var fallback
[CommandOption("foo", EnvironmentVariable = "ENV_FOO")]
public required string FooWithFallback { get; init; }
// Non-scalar option
[CommandOption("items")]
public required IReadOnlyList<string> Items { get; init; }
Argument Syntax (POSIX-style)
myapp [...directives] [command] [...parameters] [...options]
--foo bar→ option "foo" = "bar"-f bar→ option 'f' = "bar"--switch→ option "switch" (no value / boolean)-abc→ options 'a', 'b', 'c' (no value)-i file1.txt file2.txt→ option 'i' = ["file1.txt", "file2.txt"]-i file1.txt -i file2.txt→ same as above
Value Conversion
Supports out of the box:
- Primitives:
int,bool,double,ulong,char, etc. - Date/time:
DateTime,DateTimeOffset,TimeSpan - Enums: by name or numeric value
- String-initializable: types with
ctor(string)or staticParse(string)— e.g.FileInfo,Guid,BigInteger - Nullable versions of all above
- Collections:
T[],IReadOnlyList<T>,List<T>,HashSet<T>, etc.
Custom Converter
public class VectorConverter : BindingConverter<Vector2>
{
public override Vector2 Convert(string? rawValue)
{
if (string.IsNullOrWhiteSpace(rawValue))
return default;
var components = rawValue.Split('x', 'X', ';');
var x = int.Parse(components[0], CultureInfo.InvariantCulture);
var y = int.Parse(components[1], CultureInfo.InvariantCulture);
return new Vector2(x, y);
}
}
[CommandParameter(0, Converter = typeof(VectorConverter))]
public required Vector2 Point { get; init; }
Multiple / Nested Commands
Give each command a unique name. Common name segments create hierarchy:
[Command] // Default (unnamed) command
public class DefaultCommand : ICommand { ... }
[Command("cmd1")] // Child of default
public class FirstCommand : ICommand { ... }
[Command("cmd1 sub")] // Child of cmd1
public class SubCommand : ICommand { ... }
Usage: myapp cmd1 sub arg1 --opt value
Default command is optional. If absent, running without a command shows root help text.
Error Reporting
Use CommandException to report errors with specific exit codes:
throw new CommandException("Division by zero is not supported.", 133);
Exit codes should be 1–255 (8-bit unsigned) to avoid overflow on Unix.
Graceful Cancellation
public async ValueTask ExecuteAsync(IConsole console)
{
var cancellation = console.RegisterCancellationHandler();
await DoSomethingAsync(cancellation);
}
First interrupt signal triggers the token. Second interrupt force-kills.
Dependency Injection
Use UseTypeActivator(...) with Microsoft.Extensions.DependencyInjection:
await new CliApplicationBuilder()
.AddCommandsFromThisAssembly()
.UseTypeActivator(commandTypes =>
{
var services = new ServiceCollection();
services.AddSingleton<MyService>();
foreach (var commandType in commandTypes)
services.AddTransient(commandType);
return services.BuildServiceProvider();
})
.Build()
.RunAsync();
Testing
Use FakeInMemoryConsole to test commands in isolation:
Command-level test
using var console = new FakeInMemoryConsole();
var command = new ConcatCommand { Left = "foo", Right = "bar" };
await command.ExecuteAsync(console);
var stdOut = console.ReadOutputString();
Assert.Equal("foo bar", stdOut);
Application-level test
using var console = new FakeInMemoryConsole();
var app = new CliApplicationBuilder()
.AddCommand<ConcatCommand>()
.UseConsole(console)
.Build();
var args = new[] { "--left", "foo", "--right", "bar" };
var envVars = new Dictionary<string, string>();
await app.RunAsync(args, envVars);
var stdOut = console.ReadOutputString();
Assert.Equal("foo bar", stdOut);
Debug & Preview Directives
# Suspend until debugger attaches
myapp [debug] cmd -o
# Print parsed args without executing
myapp [preview] cmd arg1 -o foo
Disable in production:
new CliApplicationBuilder()
.AllowDebugMode(false)
.AllowPreviewMode(false)
.Build();
.NET Framework 4.8 Notes
- CliFx targets .NET Standard 2.0+, fully compatible with net48
requiredkeyword requires C# 11+ — on net48 (LangVersion 9.0), use properties with default values and validate inExecuteAsyncinsteadinitsetters require C# 9+ (supported on net48 with LangVersion 9.0)async Task<int> MainrequiresSystem.Threading.Tasksusing directive on net48