Update and FreshInstall both run RegisterMcpStep but the UI's Steps list never included it, so the running step never appeared and Steps.Count didn't match the executed pipeline length.
201 lines
7.2 KiB
C#
201 lines
7.2 KiB
C#
using System.Collections.ObjectModel;
|
|
using System.Diagnostics;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using ClaudeDo.Installer.Core;
|
|
using ClaudeDo.Installer.Localization;
|
|
using ClaudeDo.Installer.Steps;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
namespace ClaudeDo.Installer.Pages.InstallPage;
|
|
|
|
public partial class StepViewModel : ObservableObject
|
|
{
|
|
public string Name { get; }
|
|
|
|
[ObservableProperty] private StepStatus _status = StepStatus.Pending;
|
|
[ObservableProperty] private bool _isExpanded;
|
|
|
|
public ObservableCollection<string> Messages { get; } = [];
|
|
|
|
public StepViewModel(string name) => Name = name;
|
|
}
|
|
|
|
public partial class InstallPageViewModel : ObservableObject, IInstallerPage
|
|
{
|
|
private readonly InstallContext _context;
|
|
private readonly IServiceProvider _serviceProvider;
|
|
private InstallPageView? _view;
|
|
private CancellationTokenSource? _cts;
|
|
|
|
public string Title => TrExtension.Localizer?["installer.install.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, IServiceProvider serviceProvider)
|
|
{
|
|
_context = context;
|
|
_serviceProvider = serviceProvider;
|
|
}
|
|
|
|
public Task LoadAsync()
|
|
{
|
|
Steps.Clear();
|
|
if (_context.Mode == InstallerMode.Update)
|
|
{
|
|
Steps.Add(new StepViewModel("Stop Worker"));
|
|
Steps.Add(new StepViewModel("Download and Extract"));
|
|
Steps.Add(new StepViewModel("Register Autostart"));
|
|
Steps.Add(new StepViewModel("Register MCP with Claude"));
|
|
Steps.Add(new StepViewModel("Start Worker"));
|
|
Steps.Add(new StepViewModel("Write Install Manifest"));
|
|
Steps.Add(new StepViewModel("Register in Add/Remove Programs"));
|
|
}
|
|
else
|
|
{
|
|
Steps.Add(new StepViewModel("Download and Extract"));
|
|
Steps.Add(new StepViewModel("Write Configuration"));
|
|
Steps.Add(new StepViewModel("Initialize Database"));
|
|
Steps.Add(new StepViewModel("Register MCP with Claude"));
|
|
Steps.Add(new StepViewModel("Register Autostart"));
|
|
Steps.Add(new StepViewModel("Create Shortcuts"));
|
|
Steps.Add(new StepViewModel("Register in Add/Remove Programs"));
|
|
Steps.Add(new StepViewModel("Write Install Manifest"));
|
|
Steps.Add(new StepViewModel("Start Worker"));
|
|
}
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task ApplyAsync() => RunInstallAsync();
|
|
|
|
public bool Validate() => true;
|
|
|
|
[RelayCommand]
|
|
private async Task RunInstallAsync()
|
|
{
|
|
if (IsInstalling) return;
|
|
|
|
// Reset per-step state so a re-run starts clean instead of appending
|
|
// output to the previous run's messages.
|
|
foreach (var s in Steps)
|
|
{
|
|
s.Messages.Clear();
|
|
s.Status = StepStatus.Pending;
|
|
s.IsExpanded = false;
|
|
}
|
|
|
|
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;
|
|
|
|
// Status and output lines arrive on two separate Progress<T> channels, so a
|
|
// trailing "Running" line-message can be delivered after the step's terminal
|
|
// Done/Failed. Never let that downgrade a completed step back to Running.
|
|
if (!(step.Status is StepStatus.Done or StepStatus.Failed && p.Status is StepStatus.Running))
|
|
step.Status = p.Status;
|
|
if (p.Message is not null)
|
|
{
|
|
// Messages starting with "\r" overwrite the previous line (live progress).
|
|
if (p.Message.StartsWith('\r'))
|
|
{
|
|
var line = p.Message[1..];
|
|
if (step.Messages.Count > 0 && step.Messages[^1].StartsWith(" "))
|
|
step.Messages[^1] = line;
|
|
else
|
|
step.Messages.Add(line);
|
|
}
|
|
else
|
|
{
|
|
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
|
|
{
|
|
IEnumerable<IInstallStep> steps;
|
|
if (_context.Mode == InstallerMode.Update)
|
|
{
|
|
steps = new IInstallStep[]
|
|
{
|
|
_serviceProvider.GetRequiredService<StopWorkerStep>(),
|
|
_serviceProvider.GetRequiredService<DownloadAndExtractStep>(),
|
|
// Migrates the legacy service away and (re)registers the logon task.
|
|
_serviceProvider.GetRequiredService<RegisterAutostartStep>(),
|
|
_serviceProvider.GetRequiredService<RegisterMcpStep>(),
|
|
_serviceProvider.GetRequiredService<StartWorkerStep>(),
|
|
_serviceProvider.GetRequiredService<WriteInstallManifestStep>(),
|
|
// Refresh the bundled uninstaller exe + Add/Remove-Programs version so a
|
|
// manual update also renews the installer that bootstraps future updates.
|
|
_serviceProvider.GetRequiredService<WriteUninstallRegistryStep>(),
|
|
};
|
|
}
|
|
else
|
|
{
|
|
steps = _serviceProvider.GetServices<IInstallStep>();
|
|
}
|
|
|
|
var runner = new InstallerService(steps);
|
|
var results = await runner.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();
|
|
}
|
|
}
|
|
}
|