Files
ClaudeDo/src/ClaudeDo.Installer/Steps/WriteUninstallRegistryStep.cs
Mika Kuns f599f8d0af
Some checks failed
Release / release (push) Failing after 0s
fix(installer,worker): service hosting, dark theme, uninstall polish
Worker:
- Wire UseWindowsService + Microsoft.Extensions.Hosting.WindowsServices so
  SCM's Service Control Protocol handshake succeeds. Previously the binary
  exited immediately under sc start, leaving the service registered but
  never running.

Installer:
- Pin SDK to .NET 9 (global.json) — SDK 10 dropped win-arm from its RID
  graph, breaking restore of the WPF project; .NET 9 keeps win-arm AND
  understands the .slnx solution format.
- Force SelfContained=true and default RID=win-x64 when PublishSingleFile
  is set, so Rider Publish and CLI produce the same bundle.
- Dark theme: set Background/Foreground explicitly on WizardWindow and
  SettingsWindow roots (WPF implicit styles don't cascade to derived
  Window types). Custom ComboBox template + ComboBoxItem style so
  dropdowns honour the dark palette instead of system defaults.
- Throttle download progress to one report per MB and overwrite the same
  UI line (\r prefix marker) instead of appending per chunk.
- Register ClaudeDo in HKLM\...\Uninstall so it appears in Apps & Features.
  Copy installer into InstallDir\uninstaller\ for the UninstallString, and
  schedule a cmd.exe trampoline to handle the self-delete case when
  Apps & Features launches the copy from inside the install dir.
- Treat sc.exe stop exit 1062 (ERROR_SERVICE_NOT_ACTIVE) as success.
- Delete the uninstall registry key during UninstallRunner.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 14:19:09 +02:00

82 lines
3.4 KiB
C#

using System.IO;
using ClaudeDo.Installer.Core;
using Microsoft.Win32;
namespace ClaudeDo.Installer.Steps;
/// <summary>
/// Registers ClaudeDo under <c>HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\ClaudeDo</c>
/// so it shows up in Windows "Apps &amp; 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.
/// </summary>
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<StepResult> ExecuteAsync(InstallContext ctx, IProgress<string> 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;
}
}