Here’s the spec I’d use.
For a real .NET API CLI with Spectre.Console, I would treat Spectre as the command surface and Host.CreateApplicationBuilder(args) as the runtime backbone. Spectre’s current docs explicitly support multi-command hierarchies with AddBranch(), shared settings inheritance, DI through a registrar bridge, interceptors, async commands, logging, and in-memory testing. Microsoft’s current .NET guidance recommends the Generic Host for apps that use configuration, DI, and logging. (spectreconsole.net)
The CLI should be a task-oriented tool first, and only secondarily a “raw API caller.” Spectre’s branching model is a very good fit for that, because it gives you discoverable help at each level, aliases, descriptions, and examples. (spectreconsole.net)
Proposed command tree:
mycli
auth
login
logout
whoami
token
users
list
get
create
update
delete
projects
list
get
create
delete
request
send
config
get
set
list
path
cache
clear
doctor
version
Why this shape:
auth,config,cache, anddoctorare infrastructure branches.- resource branches (
users,projects, etc.) are the main UX. request sendis the escape hatch for unsupported endpoints.- avoid a single giant flat command list.
For user login, the default flow should be system-browser OAuth authorization code flow with PKCE, not an embedded webview. RFC 8252 is explicit that native apps should use an external user-agent, typically the browser, and not embedded user-agents; RFC 7636 defines PKCE as the mitigation for authorization-code interception in public clients. For CLI/headless/SSH scenarios, support device code flow as the fallback. (RFC Editor)
Concrete auth behavior:
mycli auth loginOpens browser, starts loopback callback listener on127.0.0.1or[::1]with an ephemeral port.mycli auth login --devicePrints verification URL + user code and polls until authorized.mycli auth whoamiShows current profile, tenant/environment, scopes, token expiry.mycli auth token --format jsonHidden or advanced-only command for scripting/debugging.mycli auth logoutDeletes local token cache and optionally calls revoke endpoint if available.
Important guardrails:
- do not embed a client secret in a widely distributed native CLI.
- if you need machine-to-machine auth, make that a separate profile using client credentials only for controlled enterprise/service scenarios, not the default interactive user flow. Microsoft’s current guidance for client-credentials explicitly says this flow is for confidential clients/service-to-service and says never to publish such credentials in source or embed them in a widely distributed native app. (Microsoft Learn)
I would also make auth a cross-cutting concern through a Spectre interceptor: anonymous commands (auth login, doctor, config path, version) bypass auth, while protected commands validate the session before execution. Spectre’s interceptor model is designed exactly for cross-cutting concerns like authentication, logging, timing, and exit-code adjustment. (spectreconsole.net)
Do not store tokens in appsettings.json, repo files, or .NET user-secrets for production use. Microsoft’s Secret Manager docs explicitly say user-secrets are not encrypted and are for development only. (Microsoft Learn)
Use this contract instead:
-
ICredentialStoreSaveAsync(profile, tokenBundle)LoadAsync(profile)DeleteAsync(profile)
-
OS-backed secure store implementation per platform
-
encrypted file fallback only when no platform store exists, with an explicit warning banner
Also separate:
- credentials/tokens → secure store
- non-secret CLI settings → config file + env vars + command-line args
- ephemeral cache → cache directory
Use the Generic Host as the composition root and Spectre as the command framework. Microsoft recommends Host.CreateApplicationBuilder for new projects, and its config guidance recommends the Generic Host approach when the app uses DI/logging/configuration. (Microsoft Learn)
Recommended project layout:
src/MyCli
Program.cs
Bootstrap/
TypeRegistrar.cs
HostingExtensions.cs
Commands/
Auth/
Users/
Projects/
Request/
Config/
Cache/
Doctor/
Interceptors/
AuthInterceptor.cs
LoggingInterceptor.cs
Settings/
GlobalSettings.cs
AuthenticatedSettings.cs
Services/
Auth/
Api/
Output/
Profiles/
Http/
Handlers/
AuthHeaderHandler.cs
CorrelationIdHandler.cs
UserAgentHandler.cs
Configuration/
CliOptions.cs
ProfileOptions.cs
Storage/
ICredentialStore.cs
IProfileStore.cs
ICacheStore.cs
Command rules:
- every command gets a small
Settingstype - common switches come from a shared base settings class
- commands stay thin; service layer does the work
- prefer
AsyncCommand<TSettings>for anything that hits network or disk
Spectre’s docs recommend AsyncCommand<TSettings> for I/O-bound operations and show how to wire Ctrl+C cancellation into RunAsync. (spectreconsole.net)
Use IHttpClientFactory with typed clients for each API area, plus delegating handlers for auth, correlation ID, user-agent, and optional idempotency headers. Microsoft’s current guidance says IHttpClientFactory gives you central client configuration, outgoing middleware via delegating handlers, handler lifetime management, and built-in logging hooks. (Microsoft Learn)
Suggested registrations:
AuthApiClientUsersApiClientProjectsApiClientRawApiClient
For resilience, add one HTTP resilience handler per client using Microsoft.Extensions.Http.Resilience; Microsoft’s current docs explicitly recommend this package for HttpClient, note that standard handlers encode common best practices, and warn against stacking multiple resilience handlers. (Microsoft Learn)
Practical default:
- connect timeout
- request timeout
- retry for transient 5xx / network faults
- 429-aware backoff when API publishes rate limits
- no retry on 4xx except explicit provider guidance
Use Spectre richly, but keep scripting stable:
- default output: human-friendly tables/panels/spinners
--json: machine-readable output--verbose/--log-level--no-color--profile--base-url--timeout
Also:
- set application name/version explicitly
- add aliases for common commands
- add examples for the help screen
- validate examples in debug builds
Spectre’s docs support SetApplicationName, SetApplicationVersion, aliases, examples, and ValidateExamples(). (spectreconsole.net)
Example global settings base:
public abstract class GlobalSettings : CommandSettings
{
[CommandOption("-p|--profile <PROFILE>")]
public string? Profile { get; init; }
[CommandOption("--base-url <URL>")]
public string? BaseUrl { get; init; }
[CommandOption("--json")]
public bool Json { get; init; }
[CommandOption("-v|--verbose")]
public bool Verbose { get; init; }
[CommandOption("--log-level <LEVEL>")]
public LogLevel? LogLevel { get; init; }
[CommandOption("--no-color")]
public bool NoColor { get; init; }
}Use centralized error handling in production with SetExceptionHandler, and use PropagateExceptions() only in debug/dev/test runs. Spectre’s current error-handling docs recommend exactly those two models and note that SetExceptionHandler is the clean central path for exit-code mapping. (spectreconsole.net)
Recommended exit codes:
0success1unexpected failure2validation / usage error3not authenticated4authentication failed / token expired and refresh failed5forbidden6not found7conflict8rate limited9network / timeout10cancelled11partial success12config/profile error
Map HTTP errors into domain exceptions first, then into exit codes centrally.
Inject IAnsiConsole into commands instead of using static AnsiConsole, and test commands with CommandAppTester. Spectre’s docs explicitly recommend injecting IAnsiConsole for testability and using Spectre.Console.Cli.Testing to run commands in memory and assert on exit code, output, and parsed settings. (spectreconsole.net)
Test layers:
- unit tests for auth/services/parsers
- command tests for argument parsing and output
- HTTP integration tests with mocked handlers
- end-to-end smoke tests for
auth,users,request send
Ship it as a .NET tool. Microsoft’s docs define .NET tools as special NuGet-packaged console apps and support both global and local installation. I’d publish:
- global tool for normal users
- local tool manifest for repo-pinned/internal team use
Also note Microsoft’s warning: .NET tools run in full trust, so this is something users install only from a trusted publisher/feed. (Microsoft Learn)
If this were my project, I would standardize on this combination:
- Spectre.Console.Cli for commands, branches, help, and UX
- Generic Host for DI/config/logging
- IHttpClientFactory + typed clients
- Microsoft.Extensions.Http.Resilience
- browser login + PKCE by default
- device code as headless fallback
- auth interceptor
- OS-backed credential store abstraction
- JSON mode for automation
- central exception-to-exit-code mapping
That gives you a CLI that feels polished for humans, but still behaves predictably in scripts and CI. It also lines up well with Spectre’s current model and Microsoft’s current hosting, HTTP, configuration, and packaging guidance. (spectreconsole.net)