CLI Framework
SaveApis.Framework.CLI provides a lightweight command-line application host built on Spectre.Console.Cli — with service injection, command discovery via attributes, and branch/sub-command support, all wired through the Installer system.
Table of Contents
Overview
When to use: Use the CLI Framework for command-line tools and utilities where users invoke specific commands with arguments.
What it provides out of the box:
- Spectre.Console.Cli command dispatch
- DI container available in commands via constructor injection
- Command registration through
RegisterCommand<T>()in installers - Hierarchical branch/sub-command support via
CommandAttribute
Key differences from Web and Console frameworks:
- Does not use
HostApplicationBuilder— uses a plainIServiceCollection+ConfigurationBuilder - No default installers (no Logging, no OpenTelemetry out of the box)
- Environment name is read from the
CLI_ENVIRONMENTenvironment variable (defaults toProduction) - Configuration is loaded from
appsettings.jsonand environment variables only ConfigureApplicationis called without aShouldInstallguard — all registered installers’ConfigureApplicationruns unconditionally
Entry Point
Start the application from Program.cs:
using SaveApis.Framework;
using SaveApis.Framework.Cli.Extensions;
await SaveApisApplication
.AsCli(
args,
(app, installer) =>
{
app.Name = "my-tool"; // used as the CLI application name
app.Namespace = "My.Org";
}
)
.AddInstaller<MyInstaller>()
.Build()
.RunAsync()
.ConfigureAwait(false);
The configure callback is optional. Unlike Web and Console, SetSoftwareAssembly() is not required since the CLI Framework does not scan assemblies for controllers or validators.
Default Installers
The CLI Framework registers no default installers. Every feature — including services, commands, and any middleware — must be added via custom installers.
Commands
CommandAttribute
Every command class must be decorated with [Command] to be registerable. The attribute declares the command name and optional branch path:
[AttributeUsage(AttributeTargets.Class)]
public class CommandAttribute(string name, params string[] branches) : Attribute
| Parameter | Description |
|---|---|
name |
The command name as typed by the user (e.g. run) |
branches |
Optional path of branch names leading to this command |
Simple Commands
A top-level command without settings:
[Command("greet")]
public class GreetCommand(IGreetingService service) : Command
{
protected override int Execute(CommandContext context, CancellationToken cancellationToken)
{
Console.WriteLine(service.GetGreeting());
return 0;
}
}
Register it in an installer:
builder.RegisterCommand<GreetCommand>();
Commands with Settings
A command that accepts arguments or options:
[Command("deploy")]
public class DeployCommand : Command<DeploySettings>
{
protected override int Execute(CommandContext context, DeploySettings settings, CancellationToken cancellationToken)
{
Console.WriteLine($"Deploying to {settings.Environment}...");
return 0;
}
}
public class DeploySettings : CommandSettings
{
[CommandArgument(0, "<environment>")]
public string Environment { get; set; } = string.Empty;
}
Register it in an installer:
builder.RegisterCommand<DeployCommand, DeploySettings>();
Branched Commands
Commands can be nested under branches by specifying the branch path in [Command]:
// Accessible as: my-tool branch1 branch2 test
[Command("test", "branch1", "branch2")]
public class TestCommand(ITestService service) : Command
{
protected override int Execute(CommandContext context, CancellationToken cancellationToken)
{
Console.WriteLine(service.GetMessage());
return 0;
}
}
Branches are created automatically if they do not already exist. Multiple commands can share the same branch path.
Custom Installers
Inherit from BaseCliInstaller to create an installer:
public class MyInstaller : BaseCliInstaller
{
public override string InstallerKey => "My";
public override void ConfigureBuilder(
SaveApisCliApplicationBuilder builder,
CliInstallerContext context)
{
// Register DI services
builder.Services.AddScoped<ITestService, TestService>();
// Register commands
builder.RegisterCommand<GreetCommand>();
builder.RegisterCommand<DeployCommand, DeploySettings>();
}
}
From the example project:
public class TestInstaller : BaseCliInstaller
{
public override string InstallerKey => "Test";
public override void ConfigureBuilder(
SaveApisCliApplicationBuilder builder,
CliInstallerContext context)
{
builder.Services.AddScoped<ITestService, TestService>();
builder.RegisterCommand<TestCommand>();
}
}
For the full installer API (lifecycle, dependencies, ShouldInstall, etc.) see the Base Framework documentation.
Note: Unlike Web and Console, there are no
CliInstallerContextextension methods for enabling/disabling built-in features, since the CLI Framework has no default installers.
Configuration
Configuration is loaded from (in order of precedence):
- Environment variables
appsettings.json(optional — no error if absent)
| Environment Variable | Description | Default |
|---|---|---|
CLI_ENVIRONMENT |
Sets the host environment name | Production |
Use builder.Configuration inside an installer to access configuration values:
public override void ConfigureBuilder(SaveApisCliApplicationBuilder builder, CliInstallerContext context)
{
var options = GetOption<MyOptions>(builder); // reads section named by InstallerKey
}