From 78831b2263c62860124ae0ebd078dc2117e26228 Mon Sep 17 00:00:00 2001 From: CubeGameLP <126233386+CubeGameLP@users.noreply.github.com> Date: Tue, 14 Apr 2026 21:01:03 +0200 Subject: [PATCH 01/32] feat(installer): add WPF installer/configurator project MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Standalone WPF app (ClaudeDo.Installer) that handles full installation and ongoing configuration of ClaudeDo. Two modes: wizard for first run, tabbed settings panel for subsequent launches. Page-based extensibility via IInstallerPage interface — adding new config sections requires only one new class. Install pipeline: dotnet publish, deploy binaries, write configs, init DB (via SchemaInitializer from ClaudeDo.Data), register Windows Service, create shortcuts. Dark theme matching the Avalonia app (forest teal accent). Co-Authored-By: Claude Opus 4.6 (1M context) --- ClaudeDo.slnx | 1 + src/ClaudeDo.Installer/App.xaml | 15 + src/ClaudeDo.Installer/App.xaml.cs | 79 +++++ .../ClaudeDo.Installer.csproj | 30 ++ src/ClaudeDo.Installer/Core/ConfigModels.cs | 109 +++++++ src/ClaudeDo.Installer/Core/DarkTitleBar.cs | 29 ++ src/ClaudeDo.Installer/Core/IInstallStep.cs | 20 ++ src/ClaudeDo.Installer/Core/IInstallerPage.cs | 16 + src/ClaudeDo.Installer/Core/InstallContext.cs | 30 ++ src/ClaudeDo.Installer/Core/InstallerMode.cs | 19 ++ .../Core/InstallerService.cs | 49 +++ .../Core/NullToCollapsedConverter.cs | 14 + src/ClaudeDo.Installer/Core/PageResolver.cs | 17 ++ src/ClaudeDo.Installer/Core/ProcessRunner.cs | 60 ++++ .../Core/StepIndicatorConverter.cs | 37 +++ .../Pages/InstallPage/InstallPageView.xaml | 101 +++++++ .../Pages/InstallPage/InstallPageView.xaml.cs | 8 + .../Pages/InstallPage/InstallPageViewModel.cs | 120 ++++++++ .../Pages/InstallPage/StepViewModel.cs | 17 ++ .../Pages/PathsPage/PathsPageView.xaml | 41 +++ .../Pages/PathsPage/PathsPageView.xaml.cs | 8 + .../Pages/PathsPage/PathsPageViewModel.cs | 74 +++++ .../Pages/ServicePage/ServicePageView.xaml | 56 ++++ .../Pages/ServicePage/ServicePageView.xaml.cs | 8 + .../Pages/ServicePage/ServicePageViewModel.cs | 88 ++++++ .../UiSettingsPage/UiSettingsPageView.xaml | 36 +++ .../UiSettingsPage/UiSettingsPageView.xaml.cs | 8 + .../UiSettingsPage/UiSettingsPageViewModel.cs | 83 ++++++ .../Pages/WelcomePage/WelcomePageView.xaml | 48 +++ .../Pages/WelcomePage/WelcomePageView.xaml.cs | 8 + .../Pages/WelcomePage/WelcomePageViewModel.cs | 106 +++++++ .../Steps/CreateShortcutsStep.cs | 91 ++++++ .../Steps/DeployBinariesStep.cs | 62 ++++ .../Steps/InitDatabaseStep.cs | 28 ++ .../Steps/PublishAppStep.cs | 20 ++ .../Steps/PublishWorkerStep.cs | 20 ++ .../Steps/RegisterServiceStep.cs | 68 +++++ .../Steps/WriteConfigStep.cs | 42 +++ src/ClaudeDo.Installer/Theme/DarkTheme.xaml | 280 ++++++++++++++++++ .../Views/SettingsViewModel.cs | 87 ++++++ .../Views/SettingsWindow.xaml | 92 ++++++ .../Views/SettingsWindow.xaml.cs | 11 + .../Views/WizardViewModel.cs | 77 +++++ .../Views/WizardWindow.xaml | 95 ++++++ .../Views/WizardWindow.xaml.cs | 11 + src/ClaudeDo.Installer/app.debug.manifest | 16 + src/ClaudeDo.Installer/app.manifest | 16 + 47 files changed, 2351 insertions(+) create mode 100644 src/ClaudeDo.Installer/App.xaml create mode 100644 src/ClaudeDo.Installer/App.xaml.cs create mode 100644 src/ClaudeDo.Installer/ClaudeDo.Installer.csproj create mode 100644 src/ClaudeDo.Installer/Core/ConfigModels.cs create mode 100644 src/ClaudeDo.Installer/Core/DarkTitleBar.cs create mode 100644 src/ClaudeDo.Installer/Core/IInstallStep.cs create mode 100644 src/ClaudeDo.Installer/Core/IInstallerPage.cs create mode 100644 src/ClaudeDo.Installer/Core/InstallContext.cs create mode 100644 src/ClaudeDo.Installer/Core/InstallerMode.cs create mode 100644 src/ClaudeDo.Installer/Core/InstallerService.cs create mode 100644 src/ClaudeDo.Installer/Core/NullToCollapsedConverter.cs create mode 100644 src/ClaudeDo.Installer/Core/PageResolver.cs create mode 100644 src/ClaudeDo.Installer/Core/ProcessRunner.cs create mode 100644 src/ClaudeDo.Installer/Core/StepIndicatorConverter.cs create mode 100644 src/ClaudeDo.Installer/Pages/InstallPage/InstallPageView.xaml create mode 100644 src/ClaudeDo.Installer/Pages/InstallPage/InstallPageView.xaml.cs create mode 100644 src/ClaudeDo.Installer/Pages/InstallPage/InstallPageViewModel.cs create mode 100644 src/ClaudeDo.Installer/Pages/InstallPage/StepViewModel.cs create mode 100644 src/ClaudeDo.Installer/Pages/PathsPage/PathsPageView.xaml create mode 100644 src/ClaudeDo.Installer/Pages/PathsPage/PathsPageView.xaml.cs create mode 100644 src/ClaudeDo.Installer/Pages/PathsPage/PathsPageViewModel.cs create mode 100644 src/ClaudeDo.Installer/Pages/ServicePage/ServicePageView.xaml create mode 100644 src/ClaudeDo.Installer/Pages/ServicePage/ServicePageView.xaml.cs create mode 100644 src/ClaudeDo.Installer/Pages/ServicePage/ServicePageViewModel.cs create mode 100644 src/ClaudeDo.Installer/Pages/UiSettingsPage/UiSettingsPageView.xaml create mode 100644 src/ClaudeDo.Installer/Pages/UiSettingsPage/UiSettingsPageView.xaml.cs create mode 100644 src/ClaudeDo.Installer/Pages/UiSettingsPage/UiSettingsPageViewModel.cs create mode 100644 src/ClaudeDo.Installer/Pages/WelcomePage/WelcomePageView.xaml create mode 100644 src/ClaudeDo.Installer/Pages/WelcomePage/WelcomePageView.xaml.cs create mode 100644 src/ClaudeDo.Installer/Pages/WelcomePage/WelcomePageViewModel.cs create mode 100644 src/ClaudeDo.Installer/Steps/CreateShortcutsStep.cs create mode 100644 src/ClaudeDo.Installer/Steps/DeployBinariesStep.cs create mode 100644 src/ClaudeDo.Installer/Steps/InitDatabaseStep.cs create mode 100644 src/ClaudeDo.Installer/Steps/PublishAppStep.cs create mode 100644 src/ClaudeDo.Installer/Steps/PublishWorkerStep.cs create mode 100644 src/ClaudeDo.Installer/Steps/RegisterServiceStep.cs create mode 100644 src/ClaudeDo.Installer/Steps/WriteConfigStep.cs create mode 100644 src/ClaudeDo.Installer/Theme/DarkTheme.xaml create mode 100644 src/ClaudeDo.Installer/Views/SettingsViewModel.cs create mode 100644 src/ClaudeDo.Installer/Views/SettingsWindow.xaml create mode 100644 src/ClaudeDo.Installer/Views/SettingsWindow.xaml.cs create mode 100644 src/ClaudeDo.Installer/Views/WizardViewModel.cs create mode 100644 src/ClaudeDo.Installer/Views/WizardWindow.xaml create mode 100644 src/ClaudeDo.Installer/Views/WizardWindow.xaml.cs create mode 100644 src/ClaudeDo.Installer/app.debug.manifest create mode 100644 src/ClaudeDo.Installer/app.manifest diff --git a/ClaudeDo.slnx b/ClaudeDo.slnx index 785aab8..4fbdd50 100644 --- a/ClaudeDo.slnx +++ b/ClaudeDo.slnx @@ -4,6 +4,7 @@ + diff --git a/src/ClaudeDo.Installer/App.xaml b/src/ClaudeDo.Installer/App.xaml new file mode 100644 index 0000000..c6c0cae --- /dev/null +++ b/src/ClaudeDo.Installer/App.xaml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/src/ClaudeDo.Installer/App.xaml.cs b/src/ClaudeDo.Installer/App.xaml.cs new file mode 100644 index 0000000..11376fa --- /dev/null +++ b/src/ClaudeDo.Installer/App.xaml.cs @@ -0,0 +1,79 @@ +using System.Windows; +using ClaudeDo.Installer.Core; +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 void OnStartup(StartupEventArgs e) + { + base.OnStartup(e); + + var mode = ModeDetector.Detect(); + _services = BuildServices(); + + Window mainWindow = mode switch + { + InstallerMode.Wizard => new WizardWindow + { + DataContext = _services.GetRequiredService() + }, + InstallerMode.Settings => new SettingsWindow + { + DataContext = _services.GetRequiredService() + }, + _ => throw new InvalidOperationException($"Unknown installer mode: {mode}") + }; + + DarkTitleBar.Apply(mainWindow); + mainWindow.Show(); + } + + protected override void OnExit(ExitEventArgs e) + { + _services?.Dispose(); + base.OnExit(e); + } + + private static ServiceProvider BuildServices() + { + var sc = new ServiceCollection(); + + // Core + sc.AddSingleton(); + sc.AddSingleton(); + sc.AddSingleton(); + + // Pages + sc.AddSingleton(); + sc.AddSingleton(); + sc.AddSingleton(); + sc.AddSingleton(); + sc.AddSingleton(); + + // Steps (registration order = execution order) + sc.AddSingleton(); + sc.AddSingleton(); + sc.AddSingleton(); + sc.AddSingleton(); + sc.AddSingleton(); + sc.AddSingleton(); + sc.AddSingleton(); + + // ViewModels + sc.AddSingleton(); + sc.AddSingleton(); + + return sc.BuildServiceProvider(); + } +} diff --git a/src/ClaudeDo.Installer/ClaudeDo.Installer.csproj b/src/ClaudeDo.Installer/ClaudeDo.Installer.csproj new file mode 100644 index 0000000..49cd431 --- /dev/null +++ b/src/ClaudeDo.Installer/ClaudeDo.Installer.csproj @@ -0,0 +1,30 @@ + + + + WinExe + net8.0-windows + true + enable + enable + + + + + app.debug.manifest + + + + + app.manifest + + + + + + + + + + + + diff --git a/src/ClaudeDo.Installer/Core/ConfigModels.cs b/src/ClaudeDo.Installer/Core/ConfigModels.cs new file mode 100644 index 0000000..124c546 --- /dev/null +++ b/src/ClaudeDo.Installer/Core/ConfigModels.cs @@ -0,0 +1,109 @@ +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; +using ClaudeDo.Data; + +namespace ClaudeDo.Installer.Core; + +/// +/// Mirrors ClaudeDo.Worker.Config.WorkerConfig JSON shape. +/// Keep in sync with src/ClaudeDo.Worker/Config/WorkerConfig.cs. +/// +public sealed class InstallerWorkerConfig +{ + [JsonPropertyName("db_path")] + public string DbPath { get; set; } = "~/.todo-app/todo.db"; + + [JsonPropertyName("sandbox_root")] + public string SandboxRoot { get; set; } = "~/.todo-app/sandbox"; + + [JsonPropertyName("log_root")] + public string LogRoot { get; set; } = "~/.todo-app/logs"; + + [JsonPropertyName("worktree_root_strategy")] + public string WorktreeRootStrategy { get; set; } = "sibling"; + + [JsonPropertyName("central_worktree_root")] + public string CentralWorktreeRoot { get; set; } = "~/.todo-app/worktrees"; + + [JsonPropertyName("queue_backstop_interval_ms")] + public int QueueBackstopIntervalMs { get; set; } = 30_000; + + [JsonPropertyName("signalr_port")] + public int SignalRPort { get; set; } = 47_821; + + [JsonPropertyName("claude_bin")] + public string ClaudeBin { get; set; } = "claude"; + + private static readonly JsonSerializerOptions ReadOpts = new() + { + ReadCommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true, + }; + + private static readonly JsonSerializerOptions WriteOpts = new() + { + WriteIndented = true, + }; + + public static InstallerWorkerConfig Load() + { + var path = Path.Combine(Paths.AppDataRoot(), "worker.config.json"); + if (!File.Exists(path)) return new(); + var json = File.ReadAllText(path); + return JsonSerializer.Deserialize(json, ReadOpts) ?? new(); + } + + public void Save() + { + var dir = Paths.AppDataRoot(); + Directory.CreateDirectory(dir); + var path = Path.Combine(dir, "worker.config.json"); + var json = JsonSerializer.Serialize(this, WriteOpts); + File.WriteAllText(path, json); + } +} + +/// +/// Mirrors ClaudeDo.Ui.AppSettings JSON shape. +/// Keep in sync with src/ClaudeDo.Ui/AppSettings.cs. +/// +public sealed class InstallerAppSettings +{ + public string DbPath { get; set; } = "~/.todo-app/todo.db"; + public string SignalRUrl { get; set; } = "http://127.0.0.1:47821/hub"; + + private static readonly JsonSerializerOptions ReadOpts = new() + { + PropertyNameCaseInsensitive = true, + }; + + private static readonly JsonSerializerOptions WriteOpts = new() + { + WriteIndented = true, + }; + + public static InstallerAppSettings Load() + { + var path = Path.Combine(Paths.AppDataRoot(), "ui.config.json"); + if (!File.Exists(path)) return new(); + try + { + var json = File.ReadAllText(path); + return JsonSerializer.Deserialize(json, ReadOpts) ?? new(); + } + catch + { + return new(); + } + } + + public void Save() + { + var dir = Paths.AppDataRoot(); + Directory.CreateDirectory(dir); + var path = Path.Combine(dir, "ui.config.json"); + var json = JsonSerializer.Serialize(this, WriteOpts); + File.WriteAllText(path, json); + } +} diff --git a/src/ClaudeDo.Installer/Core/DarkTitleBar.cs b/src/ClaudeDo.Installer/Core/DarkTitleBar.cs new file mode 100644 index 0000000..e13cf41 --- /dev/null +++ b/src/ClaudeDo.Installer/Core/DarkTitleBar.cs @@ -0,0 +1,29 @@ +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Interop; + +namespace ClaudeDo.Installer.Core; + +public static class DarkTitleBar +{ + [DllImport("dwmapi.dll", PreserveSig = true)] + private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize); + + private const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20; + + public static void Apply(Window window) + { + if (window.IsLoaded) + SetDarkMode(window); + else + window.SourceInitialized += (_, _) => SetDarkMode(window); + } + + private static void SetDarkMode(Window window) + { + var hwnd = new WindowInteropHelper(window).Handle; + if (hwnd == IntPtr.Zero) return; + int value = 1; + DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, ref value, sizeof(int)); + } +} diff --git a/src/ClaudeDo.Installer/Core/IInstallStep.cs b/src/ClaudeDo.Installer/Core/IInstallStep.cs new file mode 100644 index 0000000..67c22cd --- /dev/null +++ b/src/ClaudeDo.Installer/Core/IInstallStep.cs @@ -0,0 +1,20 @@ +namespace ClaudeDo.Installer.Core; + +public interface IInstallStep +{ + string Name { get; } + Task ExecuteAsync(InstallContext ctx, IProgress progress, CancellationToken ct); +} + +public sealed class StepResult +{ + public bool Success { get; init; } + public string? ErrorMessage { get; init; } + + public static StepResult Ok() => new() { Success = true }; + public static StepResult Fail(string error) => new() { Success = false, ErrorMessage = error }; +} + +public enum StepStatus { Pending, Running, Done, Failed, Skipped } + +public sealed record StepProgress(string StepName, StepStatus Status, string? Message = null); diff --git a/src/ClaudeDo.Installer/Core/IInstallerPage.cs b/src/ClaudeDo.Installer/Core/IInstallerPage.cs new file mode 100644 index 0000000..ef4db1c --- /dev/null +++ b/src/ClaudeDo.Installer/Core/IInstallerPage.cs @@ -0,0 +1,16 @@ +using System.Windows.Controls; + +namespace ClaudeDo.Installer.Core; + +public interface IInstallerPage +{ + string Title { get; } + string Icon { get; } + int Order { get; } + bool ShowInWizard { get; } + bool ShowInSettings { get; } + UserControl View { get; } + Task LoadAsync(); + Task ApplyAsync(); + bool Validate(); +} diff --git a/src/ClaudeDo.Installer/Core/InstallContext.cs b/src/ClaudeDo.Installer/Core/InstallContext.cs new file mode 100644 index 0000000..eafded5 --- /dev/null +++ b/src/ClaudeDo.Installer/Core/InstallContext.cs @@ -0,0 +1,30 @@ +namespace ClaudeDo.Installer.Core; + +public sealed class InstallContext +{ + // WelcomePage + public string SourceDirectory { get; set; } = ""; + public string InstallDirectory { get; set; } = @"C:\Program Files\ClaudeDo"; + + // PathsPage + public string DbPath { get; set; } = "~/.todo-app/todo.db"; + public string LogRoot { get; set; } = "~/.todo-app/logs"; + public string SandboxRoot { get; set; } = "~/.todo-app/sandbox"; + public string WorktreeRootStrategy { get; set; } = "sibling"; + public string CentralWorktreeRoot { get; set; } = "~/.todo-app/worktrees"; + + // ServicePage + public int SignalRPort { get; set; } = 47_821; + public int QueueBackstopIntervalMs { get; set; } = 30_000; + public string ClaudeBin { get; set; } = "claude"; + public string ServiceAccount { get; set; } = "LocalSystem"; + public bool AutoStart { get; set; } = true; + public int RestartDelayMs { get; set; } = 5000; + + // UiSettingsPage + public string SignalRUrl { get; set; } = "http://127.0.0.1:47821/hub"; + public string UiDbPath { get; set; } = "~/.todo-app/todo.db"; + + // InstallPage + public bool CreateDesktopShortcut { get; set; } = true; +} diff --git a/src/ClaudeDo.Installer/Core/InstallerMode.cs b/src/ClaudeDo.Installer/Core/InstallerMode.cs new file mode 100644 index 0000000..03a9eb2 --- /dev/null +++ b/src/ClaudeDo.Installer/Core/InstallerMode.cs @@ -0,0 +1,19 @@ +using System.IO; +using ClaudeDo.Data; + +namespace ClaudeDo.Installer.Core; + +public enum InstallerMode { Wizard, Settings } + +public static class ModeDetector +{ + public static InstallerMode Detect() + { + var root = Paths.AppDataRoot(); + var workerConfig = Path.Combine(root, "worker.config.json"); + var uiConfig = Path.Combine(root, "ui.config.json"); + return File.Exists(workerConfig) && File.Exists(uiConfig) + ? InstallerMode.Settings + : InstallerMode.Wizard; + } +} diff --git a/src/ClaudeDo.Installer/Core/InstallerService.cs b/src/ClaudeDo.Installer/Core/InstallerService.cs new file mode 100644 index 0000000..4d04dc9 --- /dev/null +++ b/src/ClaudeDo.Installer/Core/InstallerService.cs @@ -0,0 +1,49 @@ +namespace ClaudeDo.Installer.Core; + +public sealed class InstallerService +{ + private readonly IEnumerable _steps; + + public InstallerService(IEnumerable steps) => _steps = steps; + + public async Task> ExecuteAsync( + InstallContext ctx, + IProgress progress, + CancellationToken ct) + { + var results = new List<(IInstallStep, StepResult)>(); + + foreach (var step in _steps) + { + ct.ThrowIfCancellationRequested(); + progress.Report(new StepProgress(step.Name, StepStatus.Running)); + + var lineProgress = new Progress(msg => + progress.Report(new StepProgress(step.Name, StepStatus.Running, msg))); + + try + { + var result = await step.ExecuteAsync(ctx, lineProgress, ct); + var status = result.Success ? StepStatus.Done : StepStatus.Failed; + progress.Report(new StepProgress(step.Name, status, result.ErrorMessage)); + results.Add((step, result)); + + if (!result.Success) break; + } + catch (OperationCanceledException) + { + progress.Report(new StepProgress(step.Name, StepStatus.Failed, "Cancelled")); + results.Add((step, StepResult.Fail("Cancelled"))); + break; + } + catch (Exception ex) + { + progress.Report(new StepProgress(step.Name, StepStatus.Failed, ex.Message)); + results.Add((step, StepResult.Fail(ex.Message))); + break; + } + } + + return results; + } +} diff --git a/src/ClaudeDo.Installer/Core/NullToCollapsedConverter.cs b/src/ClaudeDo.Installer/Core/NullToCollapsedConverter.cs new file mode 100644 index 0000000..935c731 --- /dev/null +++ b/src/ClaudeDo.Installer/Core/NullToCollapsedConverter.cs @@ -0,0 +1,14 @@ +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace ClaudeDo.Installer.Core; + +public sealed class NullToCollapsedConverter : IValueConverter +{ + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + => value is null or "" ? Visibility.Collapsed : Visibility.Visible; + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + => throw new NotSupportedException(); +} diff --git a/src/ClaudeDo.Installer/Core/PageResolver.cs b/src/ClaudeDo.Installer/Core/PageResolver.cs new file mode 100644 index 0000000..b26c5ff --- /dev/null +++ b/src/ClaudeDo.Installer/Core/PageResolver.cs @@ -0,0 +1,17 @@ +namespace ClaudeDo.Installer.Core; + +public sealed class PageResolver +{ + private readonly IReadOnlyList _allPages; + + public PageResolver(IEnumerable pages) + { + _allPages = pages.OrderBy(p => p.Order).ToList(); + } + + public IReadOnlyList WizardPages => + _allPages.Where(p => p.ShowInWizard).ToList(); + + public IReadOnlyList SettingsPages => + _allPages.Where(p => p.ShowInSettings).ToList(); +} diff --git a/src/ClaudeDo.Installer/Core/ProcessRunner.cs b/src/ClaudeDo.Installer/Core/ProcessRunner.cs new file mode 100644 index 0000000..cbe94e0 --- /dev/null +++ b/src/ClaudeDo.Installer/Core/ProcessRunner.cs @@ -0,0 +1,60 @@ +using System.Diagnostics; +using System.IO; +using System.Text; + +namespace ClaudeDo.Installer.Core; + +public static class ProcessRunner +{ + public static async Task<(int ExitCode, string Output)> RunAsync( + string fileName, + string arguments, + string? workingDirectory, + IProgress? progress, + CancellationToken ct) + { + var output = new StringBuilder(); + var outputLock = new object(); + + using var process = new Process(); + process.StartInfo = new ProcessStartInfo + { + FileName = fileName, + Arguments = arguments, + WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + + if (!process.Start()) + return (-1, "Failed to start process"); + + var stdoutTask = ReadStreamAsync(process.StandardOutput, output, outputLock, progress); + var stderrTask = ReadStreamAsync(process.StandardError, output, outputLock, progress); + + using var reg = ct.Register(() => + { + try { process.Kill(entireProcessTree: true); } catch { } + }); + + await Task.WhenAll(stdoutTask, stderrTask); + await process.WaitForExitAsync(ct); + + return (process.ExitCode, output.ToString()); + } + + private static async Task ReadStreamAsync( + StreamReader reader, + StringBuilder output, + object outputLock, + IProgress? progress) + { + while (await reader.ReadLineAsync() is { } line) + { + lock (outputLock) { output.AppendLine(line); } + progress?.Report(line); + } + } +} diff --git a/src/ClaudeDo.Installer/Core/StepIndicatorConverter.cs b/src/ClaudeDo.Installer/Core/StepIndicatorConverter.cs new file mode 100644 index 0000000..14e06c8 --- /dev/null +++ b/src/ClaudeDo.Installer/Core/StepIndicatorConverter.cs @@ -0,0 +1,37 @@ +using System.Globalization; +using System.Windows; +using System.Windows.Data; +using System.Windows.Media; + +namespace ClaudeDo.Installer.Core; + +/// +/// Multi-value converter: compares the page's index with the current page index +/// to determine step indicator styling. +/// +public sealed class StepActiveConverter : IMultiValueConverter +{ + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + if (values.Length < 2 || + values[0] is not IInstallerPage page || + values[1] is not IInstallerPage currentPage) + return DependencyProperty.UnsetValue; + + var isActive = ReferenceEquals(page, currentPage); + + var key = parameter?.ToString() switch + { + "Background" => isActive ? "AccentBrush" : "WindowBgBrush", + "Foreground" => isActive ? "TextPrimaryBrush" : "TextMutedBrush", + "BorderBrush" => isActive ? "AccentBrush" : "BorderSubtleBrush", + _ => null + }; + + if (key is null) return DependencyProperty.UnsetValue; + return Application.Current.Resources[key] as SolidColorBrush ?? DependencyProperty.UnsetValue; + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + => throw new NotSupportedException(); +} diff --git a/src/ClaudeDo.Installer/Pages/InstallPage/InstallPageView.xaml b/src/ClaudeDo.Installer/Pages/InstallPage/InstallPageView.xaml new file mode 100644 index 0000000..94781c8 --- /dev/null +++ b/src/ClaudeDo.Installer/Pages/InstallPage/InstallPageView.xaml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +