Release workflow now names the installer asset ClaudeDo.Installer.exe (no version) for a permanent download URL; it is still uploaded and checksummed on every release. App + worker keep the git tag version. Removes the self-update preflight from App.OnStartup and deletes the now-dead SelfUpdater / SelfUpdatePromptWindow / SelfUpdateResult plus their tests. App-update detection is unaffected: the manifest records the release tag via DownloadAndExtractStep. Updates the installer CLAUDE.md.
158 lines
6.5 KiB
C#
158 lines
6.5 KiB
C#
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<InstallContext>();
|
|
context.InstallerVersion = GetInstallerVersion();
|
|
|
|
// Default install dir for detection — on upgrade we stay where we were.
|
|
var detector = _services.GetRequiredService<InstallModeDetector>();
|
|
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<WizardViewModel>()
|
|
},
|
|
InstallerMode.Config => new SettingsWindow
|
|
{
|
|
DataContext = _services.GetRequiredService<SettingsViewModel>()
|
|
},
|
|
_ => 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<AssemblyInformationalVersionAttribute>();
|
|
return infoAttr?.InformationalVersion ?? "0.0.0";
|
|
}
|
|
|
|
private static ServiceProvider BuildServices(ILocalizer localizer)
|
|
{
|
|
var sc = new ServiceCollection();
|
|
sc.AddSingleton(localizer);
|
|
|
|
// Core
|
|
sc.AddSingleton<InstallContext>();
|
|
sc.AddSingleton<PageResolver>();
|
|
// HTTP + release client
|
|
sc.AddSingleton(_ => new HttpClient { Timeout = TimeSpan.FromSeconds(15) });
|
|
sc.AddSingleton<IReleaseClient>(sp => new ReleaseClient(sp.GetRequiredService<HttpClient>()));
|
|
sc.AddSingleton<InstallModeDetector>();
|
|
|
|
// Pages
|
|
sc.AddSingleton<IInstallerPage, WelcomePageViewModel>();
|
|
sc.AddSingleton<IInstallerPage, PathsPageViewModel>();
|
|
sc.AddSingleton<IInstallerPage, ServicePageViewModel>();
|
|
sc.AddSingleton<IInstallerPage, UiSettingsPageViewModel>();
|
|
sc.AddSingleton<IInstallerPage, InstallPageViewModel>();
|
|
|
|
// Steps — execution order matters for the FreshInstall pipeline (IEnumerable<IInstallStep>).
|
|
// Double-registered as both IInstallStep and concrete type so the Update pipeline
|
|
// can pull them out individually via GetRequiredService<T>().
|
|
sc.AddSingleton<DownloadAndExtractStep>();
|
|
sc.AddSingleton<IInstallStep>(sp => sp.GetRequiredService<DownloadAndExtractStep>());
|
|
sc.AddSingleton<IInstallStep, WriteConfigStep>();
|
|
sc.AddSingleton<IInstallStep, InitDatabaseStep>();
|
|
sc.AddSingleton<RegisterMcpStep>();
|
|
sc.AddSingleton<IInstallStep>(sp => sp.GetRequiredService<RegisterMcpStep>());
|
|
sc.AddSingleton<RegisterAutostartStep>();
|
|
sc.AddSingleton<IInstallStep>(sp => sp.GetRequiredService<RegisterAutostartStep>());
|
|
sc.AddSingleton<IInstallStep, CreateShortcutsStep>();
|
|
sc.AddSingleton<WriteUninstallRegistryStep>();
|
|
sc.AddSingleton<IInstallStep>(sp => sp.GetRequiredService<WriteUninstallRegistryStep>());
|
|
sc.AddSingleton<WriteInstallManifestStep>();
|
|
sc.AddSingleton<IInstallStep>(sp => sp.GetRequiredService<WriteInstallManifestStep>());
|
|
// Start the worker last in the fresh pipeline (binaries + task must exist first).
|
|
sc.AddSingleton<StartWorkerStep>();
|
|
sc.AddSingleton<IInstallStep>(sp => sp.GetRequiredService<StartWorkerStep>());
|
|
|
|
// Stop — NOT registered as IInstallStep (not part of default FreshInstall pipeline).
|
|
// Pulled by Update flow + Repair/Uninstall.
|
|
sc.AddSingleton<StopWorkerStep>();
|
|
|
|
// Runners
|
|
sc.AddSingleton<UninstallRunner>();
|
|
|
|
// ViewModels
|
|
sc.AddSingleton<WizardViewModel>();
|
|
sc.AddSingleton<SettingsViewModel>();
|
|
|
|
return sc.BuildServiceProvider();
|
|
}
|
|
}
|