using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; using SVSim.Bootstrap.Importers; using SVSim.Database; namespace SVSim.Bootstrap; public static class Program { private const string DefaultConnectionString = "Host=localhost;Database=svsim;Username=postgres;password=postgres"; public static async Task Main(string[] args) { if (args.Length > 0 && (args[0] is "--help" or "-h")) { PrintUsage(); return 1; } var opts = ParseArgs(args); if (opts is null) { PrintUsage(); return 1; } if (opts.SkipCards && opts.SkipGlobals) { Console.Error.WriteLine("Both --skip-cards and --skip-globals set; nothing to do."); return 1; } Console.WriteLine($"[Bootstrap] Connection: {RedactPassword(opts.ConnectionString)}"); Console.WriteLine($"[Bootstrap] Cards file: {opts.CardsFile}"); Console.WriteLine($"[Bootstrap] Captures: {opts.CapturesDir}"); var dbOptions = new DbContextOptionsBuilder() .UseNpgsql(opts.ConnectionString) .Options; await using var context = new SVSimDbContext(NullLogger.Instance, dbOptions); // Bootstrap applies pending migrations first so it can be the very first thing run after // `dotnet ef migrations add` — no need to start the server too. Console.WriteLine("[Bootstrap] Applying pending migrations..."); await context.Database.MigrateAsync(); if (!opts.SkipCards) { await new CardImporter().ImportAsync(context, opts.CardsFile); } else { Console.WriteLine("[Bootstrap] --skip-cards set; skipping card import."); } if (!opts.SkipGlobals) { await new GlobalsImporter().ImportAllAsync(context, opts.CapturesDir); } else { Console.WriteLine("[Bootstrap] --skip-globals set; skipping globals import."); } Console.WriteLine("[Bootstrap] Complete."); return 0; } private static BootstrapOptions? ParseArgs(string[] args) { string? dataDir = null; string? cards = null; string? captures = null; string? connection = null; bool skipCards = false; bool skipGlobals = false; string? positionalCards = null; for (int i = 0; i < args.Length; i++) { string a = args[i]; switch (a) { case "--data-dir": dataDir = NextArg(args, ref i); break; case "--cards": cards = NextArg(args, ref i); break; case "--captures": captures = NextArg(args, ref i); break; case "--connection-string": connection = NextArg(args, ref i); break; case "--skip-cards": skipCards = true; break; case "--skip-globals": skipGlobals = true; break; default: // Back-compat: legacy positional form `svsim-card-import [connection]`. if (positionalCards is null && !a.StartsWith('-')) positionalCards = a; else if (connection is null && !a.StartsWith('-')) connection = a; else { Console.Error.WriteLine($"Unknown argument: {a}"); return null; } break; } } // Resolution order: // --cards beats --data-dir/cards.json beats legacy positional; // --captures beats --data-dir/prod-captures beats Bootstrap/Data/prod-captures (shipped default). string baseDir = AppContext.BaseDirectory; string shippedCaptures = Path.Combine(baseDir, "Data", "prod-captures"); string cardsFile = cards ?? (dataDir is not null ? Path.Combine(dataDir, "cards.json") : null) ?? positionalCards ?? "data_dumps/cards.json"; // Resolve captures dir, falling back to the shipped copy if the data-dir path is unset // OR points at a missing folder. (Common case: user has cards.json in data_dumps/ but // hasn't copied prod-captures/ there — the shipped snapshot is the source of truth.) string? capturesCandidate = captures ?? (dataDir is not null ? Path.Combine(dataDir, "prod-captures") : null); string capturesDir = capturesCandidate is not null && Directory.Exists(capturesCandidate) ? capturesCandidate : shippedCaptures; string connStr = connection ?? Environment.GetEnvironmentVariable("NPGSQL_CONNECTION") ?? DefaultConnectionString; return new BootstrapOptions(cardsFile, capturesDir, connStr, skipCards, skipGlobals); } private static string NextArg(string[] args, ref int i) { if (i + 1 >= args.Length) throw new ArgumentException($"Missing value for {args[i]}"); return args[++i]; } private static string RedactPassword(string conn) => System.Text.RegularExpressions.Regex.Replace(conn, "(?i)(password=)[^;]+", "$1***"); private static void PrintUsage() { Console.Error.WriteLine( "Usage: svsim-bootstrap [options]\n" + "\n" + " --data-dir Directory containing cards.json and prod-captures/\n" + " (default: ./data_dumps relative to working dir)\n" + " --cards Override path to cards.json\n" + " --captures Override path to prod-captures directory\n" + " (default: shipped Data/prod-captures next to the binary)\n" + " --connection-string Postgres connection (or NPGSQL_CONNECTION env var,\n" + $" then \"{DefaultConnectionString}\")\n" + " --skip-cards Skip card import (re-run globals only)\n" + " --skip-globals Skip globals import (cards only — legacy behavior)\n" + "\n" + "Back-compat: `svsim-bootstrap [connection]` still works (positional)."); } private sealed record BootstrapOptions( string CardsFile, string CapturesDir, string ConnectionString, bool SkipCards, bool SkipGlobals); }