Files
Joseph Doherty 32f26272ae Initial commit: Wonderware / System Platform tools and reference
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>
2026-05-03 18:22:20 -04:00

6.6 KiB
Raw Permalink Blame History

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 static Parse(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 1255 (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
  • required keyword requires C# 11+ — on net48 (LangVersion 9.0), use properties with default values and validate in ExecuteAsync instead
  • init setters require C# 9+ (supported on net48 with LangVersion 9.0)
  • async Task<int> Main requires System.Threading.Tasks using directive on net48