Add an install step and welcome-page opt-in that registers the ClaudeDo external MCP server with the Claude CLI. Failures are non-fatal and surface the manual command so a missing or old CLI never blocks the install. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
198 lines
7.0 KiB
C#
198 lines
7.0 KiB
C#
using System.Collections.ObjectModel;
|
|
using System.Diagnostics;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using ClaudeDo.Installer.Core;
|
|
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 => "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("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 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();
|
|
}
|
|
}
|
|
}
|