Files
Operation-Blue-Laminate-v2/BlueLaminate/BlueLaminate.Cli/Program.cs
2026-06-01 10:52:06 -05:00

88 lines
3.6 KiB
C#

using BlueLaminate.Cli.Commands;
using BlueLaminate.Cli.Logging;
using BlueLaminate.Core.DependencyInjection;
using BlueLaminate.EFCore.Data;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenTelemetry;
using OpenTelemetry.Resources;
using System.CommandLine;
// Generic Host = composition root. The exact same wiring a web frontend would use:
// configuration → AddBlueLaminateCore → resolve services per command scope. Args are
// deliberately NOT handed to the host (System.CommandLine owns parsing; the host's
// command-line config provider would reject bare verbs like "sync-skins"). The
// content root is the binary directory so appsettings.json is found regardless of CWD.
var builder = Host.CreateApplicationBuilder(new HostApplicationBuilderSettings
{
ContentRootPath = AppContext.BaseDirectory,
});
// Reuse the connection string stored in the EFCore project's user secrets (dev).
builder.Configuration.AddUserSecrets<SkinTrackerDbContextFactory>(optional: true);
// OpenTelemetry logging through a compact console sink that prints one
// "{utc timestamp} {message}" line per record. Swapping in an OTLP exporter later
// is a change here. ClearProviders drops the default console logger so we don't
// double-print.
builder.Logging.ClearProviders();
// IHttpClientFactory logs each request at Information under these categories; mute
// to Warning so the compact console stays one line per app message.
builder.Logging.AddFilter("System.Net.Http.HttpClient", LogLevel.Warning);
// EF Core logs every SQL command at Information; we only care about failures, so
// raise its floor to Warning (failed commands still log at Error).
builder.Logging.AddFilter("Microsoft.EntityFrameworkCore", LogLevel.Warning);
builder.Logging.AddOpenTelemetry(otel =>
{
otel.SetResourceBuilder(
ResourceBuilder.CreateDefault().AddService("BlueLaminate.Cli"));
otel.IncludeFormattedMessage = true;
otel.AddProcessor(new SimpleLogRecordExportProcessor(new CompactConsoleLogExporter()));
});
builder.Services.AddBlueLaminateCore(builder.Configuration);
using var host = builder.Build();
// This CLI builds the host but doesn't run it, so ValidateOnStart won't fire on its
// own — trigger it explicitly. Invalid configuration (e.g. CsFloat:MaxLimit out of
// range) fails fast here with a clear message instead of being silently clamped.
try
{
host.Services.GetRequiredService<IStartupValidator>().Validate();
}
catch (OptionsValidationException ex)
{
Console.Error.WriteLine("Invalid configuration:");
foreach (var failure in ex.Failures)
{
Console.Error.WriteLine($" - {failure}");
}
return 1;
}
// System.CommandLine builds the command tree, parsing, and help. Each command lives
// in its own file under Commands/ and resolves its service from a DI scope.
var root = new RootCommand("BlueLaminate CLI — Counter-Strike skin tracker tools.")
{
SyncSkinsCommand.Build(host),
FetchListingsCommand.Build(host),
SweepListingsCommand.Build(host),
SweepCatalogCommand.Build(host),
};
// Ctrl+C → cancel the action's token so long-running commands (e.g. sweep-catalog,
// which loops until stopped) unwind gracefully instead of hard-killing the process
// mid-write.
using var cts = new CancellationTokenSource();
Console.CancelKeyPress += (_, e) =>
{
e.Cancel = true; // prevent immediate termination; let the token cancel cleanly
cts.Cancel();
};
return await root.Parse(args).InvokeAsync(cancellationToken: cts.Token);