using System.Net.Http; using System.Reflection; using System.Windows; using ClaudeDo.Installer.Core; 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); _services = BuildServices(); 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; 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() { var sc = new ServiceCollection(); // 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 Task 15's Update pipeline // can pull them out individually via GetRequiredService(). sc.AddSingleton(); sc.AddSingleton(sp => sp.GetRequiredService()); sc.AddSingleton(); sc.AddSingleton(); sc.AddSingleton(); sc.AddSingleton(); sc.AddSingleton(); sc.AddSingleton(); sc.AddSingleton(sp => sp.GetRequiredService()); // Stop/Start — NOT registered as IInstallStep (not part of default FreshInstall pipeline). // Pulled by Update flow + Repair/Uninstall. sc.AddSingleton(); sc.AddSingleton(); // Runners sc.AddSingleton(); // ViewModels sc.AddSingleton(); sc.AddSingleton(); return sc.BuildServiceProvider(); } }