using System.Globalization; using System.IO; using System.Linq; using System.Net.Http; using System.Reflection; using System.Windows; using ClaudeDo.Installer.Core; using ClaudeDo.Installer.Localization; using ClaudeDo.Localization; using ClaudeDo.Releases; using ClaudeDo.Installer.Pages.InstallPage; using ClaudeDo.Installer.Pages.PathsPage; using ClaudeDo.Installer.Pages.ServicePage; using ClaudeDo.Installer.Pages.UiSettingsPage; using ClaudeDo.Installer.Pages.WelcomePage; using ClaudeDo.Installer.Steps; using ClaudeDo.Installer.Views; using Microsoft.Extensions.DependencyInjection; namespace ClaudeDo.Installer; public partial class App : Application { private ServiceProvider? _services; protected override async void OnStartup(StartupEventArgs e) { base.OnStartup(e); // --- Initialize localizer as early as possible so all windows can use {loc:Tr} --- var localesDir = Path.Combine(AppContext.BaseDirectory, "locales"); var localeStore = LocaleStore.Load(localesDir); var existingSettings = InstallerAppSettings.Load(); var initialLang = !string.IsNullOrWhiteSpace(existingSettings.Language) ? existingSettings.Language : CultureResolver.Resolve(CultureInfo.CurrentUICulture.Name, localeStore.Available.Select(l => l.Code).ToArray(), "en"); var localizer = new Localizer(localeStore, initialLang); TrExtension.Localizer = localizer; _services = BuildServices(localizer); var context = _services.GetRequiredService(); context.InstallerVersion = GetInstallerVersion(); // Default install dir for detection — on upgrade we stay where we were. var detector = _services.GetRequiredService(); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); // Read manifest up front so we can fall back to Config if the API times out // on an existing install. If the API is slow, we do NOT want to drop an // already-installed user into FreshInstall — that would risk overwriting them. var existingManifest = InstallManifestStore.TryRead(context.InstallDirectory); DetectedState state; try { state = await detector.DetectAsync(context.InstallDirectory, cts.Token); } catch (OperationCanceledException) { state = existingManifest is not null ? new DetectedState(InstallerMode.Config, existingManifest, null, null) : new DetectedState(InstallerMode.FreshInstall, null, null, null); } context.Mode = state.Mode; context.InstalledVersion = state.Existing?.Version; context.LatestVersion = state.LatestVersion; context.LatestTagUnparseable = state.LatestTagUnparseable; if (state.Existing is not null) context.InstallDirectory = state.Existing.InstallDir; Window mainWindow = state.Mode switch { InstallerMode.FreshInstall or InstallerMode.Update => new WizardWindow { DataContext = _services.GetRequiredService() }, InstallerMode.Config => new SettingsWindow { DataContext = _services.GetRequiredService() }, _ => throw new InvalidOperationException($"Unknown installer mode: {state.Mode}") }; DarkTitleBar.Apply(mainWindow); mainWindow.Show(); } protected override void OnExit(ExitEventArgs e) { _services?.Dispose(); base.OnExit(e); } private static string GetInstallerVersion() { var infoAttr = Assembly.GetExecutingAssembly() .GetCustomAttribute(); return infoAttr?.InformationalVersion ?? "0.0.0"; } private static ServiceProvider BuildServices(ILocalizer localizer) { var sc = new ServiceCollection(); sc.AddSingleton(localizer); // Core sc.AddSingleton(); sc.AddSingleton(); // HTTP + release client sc.AddSingleton(_ => new HttpClient { Timeout = TimeSpan.FromSeconds(15) }); sc.AddSingleton(sp => new ReleaseClient(sp.GetRequiredService())); sc.AddSingleton(); // Pages sc.AddSingleton(); sc.AddSingleton(); sc.AddSingleton(); sc.AddSingleton(); sc.AddSingleton(); // Steps — execution order matters for the FreshInstall pipeline (IEnumerable). // Double-registered as both IInstallStep and concrete type so the Update pipeline // can pull them out individually via GetRequiredService(). sc.AddSingleton(); sc.AddSingleton(sp => sp.GetRequiredService()); sc.AddSingleton(); sc.AddSingleton(); sc.AddSingleton(); sc.AddSingleton(sp => sp.GetRequiredService()); sc.AddSingleton(); sc.AddSingleton(sp => sp.GetRequiredService()); sc.AddSingleton(); sc.AddSingleton(); sc.AddSingleton(sp => sp.GetRequiredService()); sc.AddSingleton(); sc.AddSingleton(sp => sp.GetRequiredService()); // Start the worker last in the fresh pipeline (binaries + task must exist first). sc.AddSingleton(); sc.AddSingleton(sp => sp.GetRequiredService()); // Stop — NOT registered as IInstallStep (not part of default FreshInstall pipeline). // Pulled by Update flow + Repair/Uninstall. sc.AddSingleton(); // Runners sc.AddSingleton(); // ViewModels sc.AddSingleton(); sc.AddSingleton(); return sc.BuildServiceProvider(); } }