using System.IO; using ClaudeDo.Installer.Core; using Microsoft.Win32; namespace ClaudeDo.Installer.Steps; /// /// Registers ClaudeDo under HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\ClaudeDo /// so it shows up in Windows "Apps & Features" / "Programs and Features". /// Also copies the running installer into the install directory so there is an exe /// for UninstallString to reference after the temp-extracted single-file bundle is gone. /// public sealed class WriteUninstallRegistryStep : IInstallStep { internal const string UninstallKeyPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\ClaudeDo"; public string Name => "Register in Add/Remove Programs"; public async Task ExecuteAsync(InstallContext ctx, IProgress progress, CancellationToken ct) { var uninstallDir = Path.Combine(ctx.InstallDirectory, "uninstaller"); Directory.CreateDirectory(uninstallDir); var targetExe = Path.Combine(uninstallDir, "ClaudeDo.Installer.exe"); // Copy the running installer so Apps & Features has a stable exe to launch — // the single-file temp extract is gone once this process exits. var sourceExe = Environment.ProcessPath ?? throw new InvalidOperationException("Cannot resolve running installer path."); try { progress.Report("Copying uninstaller binary..."); File.Copy(sourceExe, targetExe, overwrite: true); } catch (Exception ex) { return StepResult.Fail($"Failed to copy uninstaller exe: {ex.Message}"); } progress.Report("Writing Add/Remove Programs entry..."); try { using var key = Registry.LocalMachine.CreateSubKey(UninstallKeyPath, writable: true); if (key is null) return StepResult.Fail("Could not open uninstall registry key (permission denied?)."); key.SetValue("DisplayName", "ClaudeDo", RegistryValueKind.String); key.SetValue("DisplayVersion", ctx.InstallerVersion ?? "0.0.0", RegistryValueKind.String); key.SetValue("Publisher", "Mika Kuns", RegistryValueKind.String); key.SetValue("InstallLocation", ctx.InstallDirectory, RegistryValueKind.String); key.SetValue("UninstallString", $"\"{targetExe}\"", RegistryValueKind.String); key.SetValue("DisplayIcon", targetExe, RegistryValueKind.String); key.SetValue("NoModify", 1, RegistryValueKind.DWord); key.SetValue("NoRepair", 1, RegistryValueKind.DWord); // Best-effort install size (KB) — scan install dir. try { var sizeKb = (int)(DirectorySizeBytes(ctx.InstallDirectory) / 1024); key.SetValue("EstimatedSize", sizeKb, RegistryValueKind.DWord); } catch { /* best-effort only */ } } catch (Exception ex) { return StepResult.Fail($"Failed to write uninstall registry: {ex.Message}"); } await Task.CompletedTask; return StepResult.Ok(); } private static long DirectorySizeBytes(string path) { long total = 0; foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)) { try { total += new FileInfo(file).Length; } catch { /* ignore */ } } return total; } }