feat(installer): add WPF installer/configurator project

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) <noreply@anthropic.com>
This commit is contained in:
CubeGameLP
2026-04-14 21:01:03 +02:00
parent 2479bb6ea1
commit 78831b2263
47 changed files with 2351 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
using System.Windows;
using ClaudeDo.Installer.Core;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace ClaudeDo.Installer.Views;
public partial class SettingsViewModel : ObservableObject
{
private readonly InstallContext _context;
public IReadOnlyList<IInstallerPage> Pages { get; }
[ObservableProperty]
private IInstallerPage? _selectedPage;
[ObservableProperty]
private string? _statusMessage;
[ObservableProperty]
private bool _isStatusError;
public SettingsViewModel(PageResolver resolver, InstallContext context)
{
Pages = resolver.SettingsPages;
_context = context;
_selectedPage = Pages.FirstOrDefault();
_ = LoadAllAsync();
}
private async Task LoadAllAsync()
{
foreach (var page in Pages)
await page.LoadAsync();
}
[RelayCommand]
private async Task Apply()
{
// Validate all pages
foreach (var page in Pages)
{
if (!page.Validate())
{
SelectedPage = page;
StatusMessage = $"Validation failed on {page.Title}. Please fix the errors.";
IsStatusError = true;
return;
}
}
// Apply all pages (writes to InstallContext)
foreach (var page in Pages)
await page.ApplyAsync();
// Write config files directly
var workerCfg = new InstallerWorkerConfig
{
DbPath = _context.DbPath,
SandboxRoot = _context.SandboxRoot,
LogRoot = _context.LogRoot,
WorktreeRootStrategy = _context.WorktreeRootStrategy,
CentralWorktreeRoot = _context.CentralWorktreeRoot,
QueueBackstopIntervalMs = _context.QueueBackstopIntervalMs,
SignalRPort = _context.SignalRPort,
ClaudeBin = _context.ClaudeBin,
};
workerCfg.Save();
var uiCfg = new InstallerAppSettings
{
DbPath = _context.UiDbPath,
SignalRUrl = _context.SignalRUrl,
};
uiCfg.Save();
StatusMessage = "Settings saved successfully.";
IsStatusError = false;
}
[RelayCommand]
private void Close()
{
Application.Current.Shutdown();
}
}

View File

@@ -0,0 +1,92 @@
<Window x:Class="ClaudeDo.Installer.Views.SettingsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:ClaudeDo.Installer.Views"
Title="ClaudeDo Settings"
Width="720" Height="520"
MinWidth="620" MinHeight="460"
WindowStartupLocation="CenterScreen"
d:DataContext="{d:DesignInstance views:SettingsViewModel}"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Main content area -->
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="180"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Sidebar -->
<Border Grid.Column="0" Background="{StaticResource SidebarBgBrush}"
Padding="8,12">
<ListBox ItemsSource="{Binding Pages}"
SelectedItem="{Binding SelectedPage}"
HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Icon}" FontSize="14" Margin="0,0,8,0"
VerticalAlignment="Center"/>
<TextBlock Text="{Binding Title}" FontSize="13"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
<!-- Separator -->
<Border Grid.Column="1" Width="1" Background="{StaticResource BorderSubtleBrush}"/>
<!-- Page content -->
<Border Grid.Column="2" Padding="24,20">
<ContentControl Content="{Binding SelectedPage.View}"/>
</Border>
</Grid>
<!-- Bottom Bar -->
<Border Grid.Row="1" Background="{StaticResource IslandBgBrush}"
BorderBrush="{StaticResource BorderSubtleBrush}" BorderThickness="0,1,0,0"
Padding="20,12">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Status message -->
<TextBlock Grid.Column="0" Text="{Binding StatusMessage}"
VerticalAlignment="Center" FontSize="12">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="{StaticResource AccentBrush}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsStatusError}" Value="True">
<Setter Property="Foreground" Value="{StaticResource ErrorBrush}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<Button Grid.Column="1" Content="Close"
Command="{Binding CloseCommand}"
Margin="0,0,8,0" MinWidth="80"/>
<Button Grid.Column="2" Content="Save &amp; Apply"
Command="{Binding ApplyCommand}"
Style="{StaticResource AccentButton}"
MinWidth="100"/>
</Grid>
</Border>
</Grid>
</Window>

View File

@@ -0,0 +1,11 @@
using System.Windows;
namespace ClaudeDo.Installer.Views;
public partial class SettingsWindow : Window
{
public SettingsWindow()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,77 @@
using System.Windows;
using ClaudeDo.Installer.Core;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace ClaudeDo.Installer.Views;
public partial class WizardViewModel : ObservableObject
{
private readonly InstallContext _context;
public IReadOnlyList<IInstallerPage> Pages { get; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanGoBack))]
[NotifyPropertyChangedFor(nameof(IsLastPage))]
[NotifyPropertyChangedFor(nameof(NextButtonText))]
[NotifyPropertyChangedFor(nameof(CurrentPage))]
private int _currentPageIndex;
public IInstallerPage CurrentPage => Pages[CurrentPageIndex];
public bool CanGoBack => CurrentPageIndex > 0;
public bool IsLastPage => CurrentPageIndex == Pages.Count - 1;
public string NextButtonText => IsLastPage ? "Install" : "Next \u2192";
[ObservableProperty]
private string? _validationError;
public WizardViewModel(PageResolver resolver, InstallContext context)
{
Pages = resolver.WizardPages;
_context = context;
if (Pages.Count > 0)
_ = InitAsync();
}
private async Task InitAsync()
{
try { await Pages[0].LoadAsync(); }
catch { /* first page loads with defaults on error */ }
}
[RelayCommand]
private async Task GoBack()
{
if (!CanGoBack) return;
CurrentPageIndex--;
await CurrentPage.LoadAsync();
ValidationError = null;
}
[RelayCommand]
private async Task GoNext()
{
if (!CurrentPage.Validate())
{
ValidationError = "Please fix the highlighted errors before continuing.";
return;
}
ValidationError = null;
await CurrentPage.ApplyAsync();
if (CurrentPageIndex < Pages.Count - 1)
{
CurrentPageIndex++;
await CurrentPage.LoadAsync();
}
}
[RelayCommand]
private void Close()
{
Application.Current.Shutdown();
}
}

View File

@@ -0,0 +1,95 @@
<Window x:Class="ClaudeDo.Installer.Views.WizardWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:ClaudeDo.Installer.Views"
Title="ClaudeDo Installer"
Width="720" Height="520"
MinWidth="620" MinHeight="460"
WindowStartupLocation="CenterScreen"
d:DataContext="{d:DesignInstance views:WizardViewModel}"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Step Indicator -->
<Border Grid.Row="0" Background="{StaticResource IslandBgBrush}"
BorderBrush="{StaticResource BorderSubtleBrush}" BorderThickness="0,0,0,1"
Padding="20,14">
<ItemsControl ItemsSource="{Binding Pages}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border x:Name="StepBorder" CornerRadius="4" Padding="10,5" Margin="0,0,6,0"
BorderThickness="1">
<Border.Background>
<MultiBinding Converter="{StaticResource StepActiveConverter}" ConverterParameter="Background">
<Binding/>
<Binding RelativeSource="{RelativeSource AncestorType=Window}" Path="DataContext.CurrentPage"/>
</MultiBinding>
</Border.Background>
<Border.BorderBrush>
<MultiBinding Converter="{StaticResource StepActiveConverter}" ConverterParameter="BorderBrush">
<Binding/>
<Binding RelativeSource="{RelativeSource AncestorType=Window}" Path="DataContext.CurrentPage"/>
</MultiBinding>
</Border.BorderBrush>
<TextBlock Text="{Binding Title}" FontSize="12">
<TextBlock.Foreground>
<MultiBinding Converter="{StaticResource StepActiveConverter}" ConverterParameter="Foreground">
<Binding/>
<Binding RelativeSource="{RelativeSource AncestorType=Window}" Path="DataContext.CurrentPage"/>
</MultiBinding>
</TextBlock.Foreground>
</TextBlock>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
<!-- Page Content -->
<Border Grid.Row="1" Padding="24,20">
<ContentControl Content="{Binding CurrentPage.View}"/>
</Border>
<!-- Bottom Bar -->
<Border Grid.Row="2" Background="{StaticResource IslandBgBrush}"
BorderBrush="{StaticResource BorderSubtleBrush}" BorderThickness="0,1,0,0"
Padding="20,12">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Validation error -->
<TextBlock Grid.Column="0" Text="{Binding ValidationError}"
Foreground="{StaticResource ErrorBrush}"
VerticalAlignment="Center" FontSize="12"
Visibility="{Binding ValidationError, Converter={StaticResource NullToCollapsedConverter}}"/>
<Button Grid.Column="1" Content="Back"
Command="{Binding GoBackCommand}"
IsEnabled="{Binding CanGoBack}"
Margin="0,0,8,0" MinWidth="80"/>
<Button Grid.Column="2" Content="{Binding NextButtonText}"
Command="{Binding GoNextCommand}"
Style="{StaticResource AccentButton}"
MinWidth="100"/>
</Grid>
</Border>
</Grid>
</Window>

View File

@@ -0,0 +1,11 @@
using System.Windows;
namespace ClaudeDo.Installer.Views;
public partial class WizardWindow : Window
{
public WizardWindow()
{
InitializeComponent();
}
}