using System.Net.Http; using System.Reflection; using System.Windows; using ClaudeDo.Installer.Core; 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); // --- Self-update pre-flight --- // Resolve current exe path. Assembly.Location may point to a .dll for apphost-based // .NET apps; swap to the .exe companion when that happens. var currentExePath = Assembly.GetEntryAssembly()!.Location; if (currentExePath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) { currentExePath = System.IO.Path.ChangeExtension(currentExePath, ".exe"); } // Arg form: --replace-self "" var replaceSelfIndex = Array.FindIndex(e.Args, a => a.Equals("--replace-self", StringComparison.OrdinalIgnoreCase)); if (replaceSelfIndex >= 0 && replaceSelfIndex + 1 < e.Args.Length) { var oldPath = e.Args[replaceSelfIndex + 1]; var relaunched = await SelfUpdater.HandleReplaceSelfAsync( oldPath: oldPath, currentExePath: currentExePath, launchProcess: path => { try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path) { UseShellExecute = true }); return true; } catch { return false; } }); if (relaunched) { Shutdown(0); return; } // Replacement failed — fall through to normal wizard from the temp location. } else { // Normal launch: check for a newer installer. using var selfUpdateHttp = new HttpClient { Timeout = TimeSpan.FromSeconds(10) }; var selfUpdateReleases = new ReleaseClient(selfUpdateHttp); var currentVersion = GetInstallerVersion(); var decision = await SelfUpdater.DecideUpdateAsync(selfUpdateReleases, currentVersion, CancellationToken.None); if (decision.Kind == SelfUpdateDecisionKind.UpdateAvailable) { var prompt = new SelfUpdatePromptWindow(currentVersion, decision.LatestVersion!); DarkTitleBar.Apply(prompt); var ok = prompt.ShowDialog() == true; if (!ok) { Shutdown(0); return; } if (prompt.Choice == SelfUpdateChoice.Update) { prompt.ShowProgress("Downloading..."); var tempDir = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "ClaudeDo.Installer.Update"); var verifiedPath = await SelfUpdater.DownloadAndVerifyAsync( selfUpdateReleases, decision.InstallerAsset!, decision.ChecksumsAsset!, tempDir, new Progress(_ => { }), CancellationToken.None); if (verifiedPath is null) { MessageBox.Show(prompt, "Update download or verification failed. Continuing with current installer.", "ClaudeDo Installer", MessageBoxButton.OK, MessageBoxImage.Warning); } else { try { var psi = new System.Diagnostics.ProcessStartInfo(verifiedPath) { UseShellExecute = true, }; psi.ArgumentList.Add("--replace-self"); psi.ArgumentList.Add(currentExePath); System.Diagnostics.Process.Start(psi); Shutdown(0); return; } catch (Exception ex) { MessageBox.Show(prompt, "Failed to launch updated installer: " + ex.Message + "\nContinuing with current installer.", "ClaudeDo Installer", MessageBoxButton.OK, MessageBoxImage.Warning); } } } // SelfUpdateChoice.Continue — fall through to normal wizard. } // No-update or check failed — fall through to normal wizard. } // --- Existing wizard start-up unchanged below this line --- _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; 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() { 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 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(); 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(); } }