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:
87
src/ClaudeDo.Installer/Views/SettingsViewModel.cs
Normal file
87
src/ClaudeDo.Installer/Views/SettingsViewModel.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
92
src/ClaudeDo.Installer/Views/SettingsWindow.xaml
Normal file
92
src/ClaudeDo.Installer/Views/SettingsWindow.xaml
Normal 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 & Apply"
|
||||
Command="{Binding ApplyCommand}"
|
||||
Style="{StaticResource AccentButton}"
|
||||
MinWidth="100"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
11
src/ClaudeDo.Installer/Views/SettingsWindow.xaml.cs
Normal file
11
src/ClaudeDo.Installer/Views/SettingsWindow.xaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace ClaudeDo.Installer.Views;
|
||||
|
||||
public partial class SettingsWindow : Window
|
||||
{
|
||||
public SettingsWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
77
src/ClaudeDo.Installer/Views/WizardViewModel.cs
Normal file
77
src/ClaudeDo.Installer/Views/WizardViewModel.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
95
src/ClaudeDo.Installer/Views/WizardWindow.xaml
Normal file
95
src/ClaudeDo.Installer/Views/WizardWindow.xaml
Normal 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>
|
||||
11
src/ClaudeDo.Installer/Views/WizardWindow.xaml.cs
Normal file
11
src/ClaudeDo.Installer/Views/WizardWindow.xaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace ClaudeDo.Installer.Views;
|
||||
|
||||
public partial class WizardWindow : Window
|
||||
{
|
||||
public WizardWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user