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,120 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using ClaudeDo.Installer.Core;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace ClaudeDo.Installer.Pages.InstallPage;
public partial class InstallPageViewModel : ObservableObject, IInstallerPage
{
private readonly InstallContext _context;
private readonly InstallerService _installerService;
private InstallPageView? _view;
private CancellationTokenSource? _cts;
public string Title => "Install";
public string Icon => "\uE896";
public int Order => 99;
public bool ShowInWizard => true;
public bool ShowInSettings => false;
public UserControl View => _view ??= new InstallPageView { DataContext = this };
public ObservableCollection<StepViewModel> Steps { get; } = [];
[ObservableProperty] private bool _isInstalling;
[ObservableProperty] private bool _isComplete;
[ObservableProperty] private bool _hasErrors;
[ObservableProperty] private double _overallProgress;
public InstallPageViewModel(InstallContext context, InstallerService installerService)
{
_context = context;
_installerService = installerService;
}
public Task LoadAsync()
{
Steps.Clear();
Steps.Add(new StepViewModel("Publish ClaudeDo.App"));
Steps.Add(new StepViewModel("Publish ClaudeDo.Worker"));
Steps.Add(new StepViewModel("Deploy Binaries"));
Steps.Add(new StepViewModel("Write Configuration"));
Steps.Add(new StepViewModel("Initialize Database"));
Steps.Add(new StepViewModel("Register Windows Service"));
Steps.Add(new StepViewModel("Create Shortcuts"));
return Task.CompletedTask;
}
public Task ApplyAsync() => RunInstallAsync();
public bool Validate() => true;
[RelayCommand]
private async Task RunInstallAsync()
{
if (IsInstalling) return;
IsInstalling = true;
IsComplete = false;
HasErrors = false;
OverallProgress = 0;
_cts = new CancellationTokenSource();
var progress = new Progress<StepProgress>(p =>
{
var step = Steps.FirstOrDefault(s => s.Name == p.StepName);
if (step is null) return;
step.Status = p.Status;
if (p.Message is not null)
step.Messages.Add(p.Message);
if (p.Status is StepStatus.Running && !step.IsExpanded)
step.IsExpanded = true;
if (p.Status is StepStatus.Done or StepStatus.Failed)
{
var completed = Steps.Count(s => s.Status is StepStatus.Done or StepStatus.Failed);
OverallProgress = (double)completed / Steps.Count * 100;
}
});
try
{
var results = await _installerService.ExecuteAsync(_context, progress, _cts.Token);
HasErrors = results.Any(r => !r.Result.Success);
}
catch (OperationCanceledException)
{
HasErrors = true;
}
finally
{
IsInstalling = false;
IsComplete = true;
_cts.Dispose();
_cts = null;
}
}
[RelayCommand]
private void CancelInstall()
{
_cts?.Cancel();
}
[RelayCommand]
private void LaunchApp()
{
var appExe = System.IO.Path.Combine(_context.InstallDirectory, "app", "ClaudeDo.App.exe");
if (System.IO.File.Exists(appExe))
{
Process.Start(new ProcessStartInfo(appExe) { UseShellExecute = true });
Application.Current.Shutdown();
}
}
}