using System.CommandLine; using StellaOps.Cli.Extensions; namespace StellaOps.Cli.Commands; internal static class OfflineCommandGroup { internal static Command BuildOfflineCommand( IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var offline = new Command("offline", "Air-gap and offline kit operations."); offline.Add(BuildOfflineImportCommand(services, verboseOption, cancellationToken)); offline.Add(BuildOfflineStatusCommand(services, verboseOption, cancellationToken)); return offline; } private static Command BuildOfflineImportCommand( IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var tenantOption = new Option("--tenant") { Description = "Tenant context for the import (defaults to profile/ENV)." }; var bundleOption = new Option("--bundle", new[] { "-b" }) { Description = "Path to the offline kit payload bundle (.tar.zst).", Required = true }; var manifestOption = new Option("--manifest", new[] { "-m" }) { Description = "Path to offline manifest JSON (defaults to manifest.json next to the bundle)." }; var verifyDsseOption = new Option("--verify-dsse") { Description = "Verify DSSE signature on the kit statement." }.SetDefaultValue(true); var verifyRekorOption = new Option("--verify-rekor") { Description = "Verify Rekor receipt (offline mode)." }.SetDefaultValue(true); var trustRootOption = new Option("--trust-root") { Description = "Path to trust root public key file for DSSE verification." }; var forceActivateOption = new Option("--force-activate") { Description = "Override monotonicity check (requires justification)." }; var forceReasonOption = new Option("--force-reason") { Description = "Justification for force activation (required with --force-activate)." }; var dryRunOption = new Option("--dry-run") { Description = "Validate the kit without activating." }; var outputOption = new Option("--output", new[] { "-o" }) { Description = "Output format: table (default), json." }.SetDefaultValue("table").FromAmong("table", "json"); var command = new Command("import", "Import an offline kit with verification.") { tenantOption, bundleOption, manifestOption, verifyDsseOption, verifyRekorOption, trustRootOption, forceActivateOption, forceReasonOption, dryRunOption, outputOption, verboseOption }; command.SetAction(parseResult => { var tenant = parseResult.GetValue(tenantOption); var bundle = parseResult.GetValue(bundleOption) ?? string.Empty; var manifest = parseResult.GetValue(manifestOption); var verifyDsse = parseResult.GetValue(verifyDsseOption); var verifyRekor = parseResult.GetValue(verifyRekorOption); var trustRoot = parseResult.GetValue(trustRootOption); var forceActivate = parseResult.GetValue(forceActivateOption); var forceReason = parseResult.GetValue(forceReasonOption); var dryRun = parseResult.GetValue(dryRunOption); var output = parseResult.GetValue(outputOption) ?? "table"; var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleOfflineImportAsync( services, tenant, bundle, manifest, verifyDsse, verifyRekor, trustRoot, forceActivate, forceReason, dryRun, output, verbose, cancellationToken); }); return command; } private static Command BuildOfflineStatusCommand( IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var tenantOption = new Option("--tenant") { Description = "Tenant context for the status (defaults to profile/ENV)." }; var outputOption = new Option("--output", new[] { "-o" }) { Description = "Output format: table (default), json." }.SetDefaultValue("table").FromAmong("table", "json"); var command = new Command("status", "Display current offline kit status.") { tenantOption, outputOption, verboseOption }; command.SetAction(parseResult => { var tenant = parseResult.GetValue(tenantOption); var output = parseResult.GetValue(outputOption) ?? "table"; var verbose = parseResult.GetValue(verboseOption); return CommandHandlers.HandleOfflineStatusAsync( services, tenant, output, verbose, cancellationToken); }); return command; } }