diff --git a/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj b/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj index c296826..9fddf53 100644 --- a/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj +++ b/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj @@ -10,6 +10,7 @@ + diff --git a/src/ClaudeDo.Ui/Services/InstallerLocator.cs b/src/ClaudeDo.Ui/Services/InstallerLocator.cs new file mode 100644 index 0000000..de15542 --- /dev/null +++ b/src/ClaudeDo.Ui/Services/InstallerLocator.cs @@ -0,0 +1,47 @@ +namespace ClaudeDo.Ui.Services; + +public sealed class InstallerLocator +{ + private const string InstallJson = "install.json"; + private const string InstallerExe = "ClaudeDo.Installer.exe"; + private const string UninstallerSubdir = "uninstaller"; + + public string? Find() + => FindByWalkingUp(AppContext.BaseDirectory) ?? FindByRegistry(); + + public string? FindByWalkingUp(string startDir) + { + var dir = new DirectoryInfo(startDir); + while (dir is not null) + { + var manifest = Path.Combine(dir.FullName, InstallJson); + if (File.Exists(manifest)) + { + var candidate = Path.Combine(dir.FullName, UninstallerSubdir, InstallerExe); + return File.Exists(candidate) ? candidate : null; + } + dir = dir.Parent; + } + return null; + } + + [System.Runtime.Versioning.SupportedOSPlatform("windows")] + public string? FindByRegistry() + { + if (!OperatingSystem.IsWindows()) return null; + + try + { + using var key = Microsoft.Win32.Registry.LocalMachine + .OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\ClaudeDo"); + var location = key?.GetValue("InstallLocation") as string; + if (string.IsNullOrEmpty(location)) return null; + var candidate = Path.Combine(location, UninstallerSubdir, InstallerExe); + return File.Exists(candidate) ? candidate : null; + } + catch + { + return null; + } + } +} diff --git a/tests/ClaudeDo.Ui.Tests/Services/InstallerLocatorTests.cs b/tests/ClaudeDo.Ui.Tests/Services/InstallerLocatorTests.cs new file mode 100644 index 0000000..4ddc948 --- /dev/null +++ b/tests/ClaudeDo.Ui.Tests/Services/InstallerLocatorTests.cs @@ -0,0 +1,58 @@ +using ClaudeDo.Ui.Services; + +namespace ClaudeDo.Ui.Tests.Services; + +public class InstallerLocatorTests : IDisposable +{ + private readonly string _root; + + public InstallerLocatorTests() + { + _root = Path.Combine(Path.GetTempPath(), "ClaudeDo.Ui.Tests-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(_root); + } + + public void Dispose() { try { Directory.Delete(_root, true); } catch { } } + + [Fact] + public void Find_WalkUpFromAppDir_ToInstallJsonSibling() + { + var installDir = Path.Combine(_root, "ClaudeDo"); + var appDir = Path.Combine(installDir, "app"); + var uninstallerDir = Path.Combine(installDir, "uninstaller"); + Directory.CreateDirectory(appDir); + Directory.CreateDirectory(uninstallerDir); + + File.WriteAllText(Path.Combine(installDir, "install.json"), "{}"); + var installerPath = Path.Combine(uninstallerDir, "ClaudeDo.Installer.exe"); + File.WriteAllText(installerPath, "x"); + + var locator = new InstallerLocator(); + var found = locator.FindByWalkingUp(appDir); + + Assert.Equal(installerPath, found); + } + + [Fact] + public void Find_ReturnsNullWhenNoInstallJson() + { + var appDir = Path.Combine(_root, "somewhere", "app"); + Directory.CreateDirectory(appDir); + + var locator = new InstallerLocator(); + Assert.Null(locator.FindByWalkingUp(appDir)); + } + + [Fact] + public void Find_ReturnsNullWhenInstallerMissingFromUninstallerDir() + { + var installDir = Path.Combine(_root, "ClaudeDo"); + var appDir = Path.Combine(installDir, "app"); + Directory.CreateDirectory(appDir); + Directory.CreateDirectory(Path.Combine(installDir, "uninstaller")); + File.WriteAllText(Path.Combine(installDir, "install.json"), "{}"); + + var locator = new InstallerLocator(); + Assert.Null(locator.FindByWalkingUp(appDir)); + } +}