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:
@@ -4,6 +4,7 @@
|
|||||||
<Project Path="src/ClaudeDo.Data/ClaudeDo.Data.csproj" />
|
<Project Path="src/ClaudeDo.Data/ClaudeDo.Data.csproj" />
|
||||||
<Project Path="src/ClaudeDo.Ui/ClaudeDo.Ui.csproj" />
|
<Project Path="src/ClaudeDo.Ui/ClaudeDo.Ui.csproj" />
|
||||||
<Project Path="src/ClaudeDo.Worker/ClaudeDo.Worker.csproj" />
|
<Project Path="src/ClaudeDo.Worker/ClaudeDo.Worker.csproj" />
|
||||||
|
<Project Path="src/ClaudeDo.Installer/ClaudeDo.Installer.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/tests/">
|
<Folder Name="/tests/">
|
||||||
<Project Path="tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj" />
|
<Project Path="tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj" />
|
||||||
|
|||||||
15
src/ClaudeDo.Installer/App.xaml
Normal file
15
src/ClaudeDo.Installer/App.xaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<Application x:Class="ClaudeDo.Installer.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:core="clr-namespace:ClaudeDo.Installer.Core">
|
||||||
|
<Application.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<ResourceDictionary Source="Theme/DarkTheme.xaml"/>
|
||||||
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
<core:NullToCollapsedConverter x:Key="NullToCollapsedConverter"/>
|
||||||
|
<core:StepActiveConverter x:Key="StepActiveConverter"/>
|
||||||
|
<BooleanToVisibilityConverter x:Key="BoolToVisConverter"/>
|
||||||
|
</ResourceDictionary>
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
||||||
79
src/ClaudeDo.Installer/App.xaml.cs
Normal file
79
src/ClaudeDo.Installer/App.xaml.cs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using ClaudeDo.Installer.Core;
|
||||||
|
using ClaudeDo.Installer.Pages.InstallPage;
|
||||||
|
using ClaudeDo.Installer.Pages.PathsPage;
|
||||||
|
using ClaudeDo.Installer.Pages.ServicePage;
|
||||||
|
using ClaudeDo.Installer.Pages.UiSettingsPage;
|
||||||
|
using ClaudeDo.Installer.Pages.WelcomePage;
|
||||||
|
using ClaudeDo.Installer.Steps;
|
||||||
|
using ClaudeDo.Installer.Views;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Installer;
|
||||||
|
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
private ServiceProvider? _services;
|
||||||
|
|
||||||
|
protected override void OnStartup(StartupEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnStartup(e);
|
||||||
|
|
||||||
|
var mode = ModeDetector.Detect();
|
||||||
|
_services = BuildServices();
|
||||||
|
|
||||||
|
Window mainWindow = mode switch
|
||||||
|
{
|
||||||
|
InstallerMode.Wizard => new WizardWindow
|
||||||
|
{
|
||||||
|
DataContext = _services.GetRequiredService<WizardViewModel>()
|
||||||
|
},
|
||||||
|
InstallerMode.Settings => new SettingsWindow
|
||||||
|
{
|
||||||
|
DataContext = _services.GetRequiredService<SettingsViewModel>()
|
||||||
|
},
|
||||||
|
_ => throw new InvalidOperationException($"Unknown installer mode: {mode}")
|
||||||
|
};
|
||||||
|
|
||||||
|
DarkTitleBar.Apply(mainWindow);
|
||||||
|
mainWindow.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnExit(ExitEventArgs e)
|
||||||
|
{
|
||||||
|
_services?.Dispose();
|
||||||
|
base.OnExit(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ServiceProvider BuildServices()
|
||||||
|
{
|
||||||
|
var sc = new ServiceCollection();
|
||||||
|
|
||||||
|
// Core
|
||||||
|
sc.AddSingleton<InstallContext>();
|
||||||
|
sc.AddSingleton<PageResolver>();
|
||||||
|
sc.AddSingleton<InstallerService>();
|
||||||
|
|
||||||
|
// Pages
|
||||||
|
sc.AddSingleton<IInstallerPage, WelcomePageViewModel>();
|
||||||
|
sc.AddSingleton<IInstallerPage, PathsPageViewModel>();
|
||||||
|
sc.AddSingleton<IInstallerPage, ServicePageViewModel>();
|
||||||
|
sc.AddSingleton<IInstallerPage, UiSettingsPageViewModel>();
|
||||||
|
sc.AddSingleton<IInstallerPage, InstallPageViewModel>();
|
||||||
|
|
||||||
|
// Steps (registration order = execution order)
|
||||||
|
sc.AddSingleton<IInstallStep, PublishAppStep>();
|
||||||
|
sc.AddSingleton<IInstallStep, PublishWorkerStep>();
|
||||||
|
sc.AddSingleton<IInstallStep, DeployBinariesStep>();
|
||||||
|
sc.AddSingleton<IInstallStep, WriteConfigStep>();
|
||||||
|
sc.AddSingleton<IInstallStep, InitDatabaseStep>();
|
||||||
|
sc.AddSingleton<IInstallStep, RegisterServiceStep>();
|
||||||
|
sc.AddSingleton<IInstallStep, CreateShortcutsStep>();
|
||||||
|
|
||||||
|
// ViewModels
|
||||||
|
sc.AddSingleton<WizardViewModel>();
|
||||||
|
sc.AddSingleton<SettingsViewModel>();
|
||||||
|
|
||||||
|
return sc.BuildServiceProvider();
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/ClaudeDo.Installer/ClaudeDo.Installer.csproj
Normal file
30
src/ClaudeDo.Installer/ClaudeDo.Installer.csproj
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<!-- Debug: asInvoker so Rider/VS can debug without elevation -->
|
||||||
|
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
|
||||||
|
<ApplicationManifest>app.debug.manifest</ApplicationManifest>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<!-- Release: requireAdministrator for service registration + shortcuts -->
|
||||||
|
<PropertyGroup Condition="'$(Configuration)' != 'Debug'">
|
||||||
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\ClaudeDo.Data\ClaudeDo.Data.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
109
src/ClaudeDo.Installer/Core/ConfigModels.cs
Normal file
109
src/ClaudeDo.Installer/Core/ConfigModels.cs
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using ClaudeDo.Data;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Installer.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mirrors ClaudeDo.Worker.Config.WorkerConfig JSON shape.
|
||||||
|
/// Keep in sync with src/ClaudeDo.Worker/Config/WorkerConfig.cs.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class InstallerWorkerConfig
|
||||||
|
{
|
||||||
|
[JsonPropertyName("db_path")]
|
||||||
|
public string DbPath { get; set; } = "~/.todo-app/todo.db";
|
||||||
|
|
||||||
|
[JsonPropertyName("sandbox_root")]
|
||||||
|
public string SandboxRoot { get; set; } = "~/.todo-app/sandbox";
|
||||||
|
|
||||||
|
[JsonPropertyName("log_root")]
|
||||||
|
public string LogRoot { get; set; } = "~/.todo-app/logs";
|
||||||
|
|
||||||
|
[JsonPropertyName("worktree_root_strategy")]
|
||||||
|
public string WorktreeRootStrategy { get; set; } = "sibling";
|
||||||
|
|
||||||
|
[JsonPropertyName("central_worktree_root")]
|
||||||
|
public string CentralWorktreeRoot { get; set; } = "~/.todo-app/worktrees";
|
||||||
|
|
||||||
|
[JsonPropertyName("queue_backstop_interval_ms")]
|
||||||
|
public int QueueBackstopIntervalMs { get; set; } = 30_000;
|
||||||
|
|
||||||
|
[JsonPropertyName("signalr_port")]
|
||||||
|
public int SignalRPort { get; set; } = 47_821;
|
||||||
|
|
||||||
|
[JsonPropertyName("claude_bin")]
|
||||||
|
public string ClaudeBin { get; set; } = "claude";
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions ReadOpts = new()
|
||||||
|
{
|
||||||
|
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||||
|
AllowTrailingCommas = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions WriteOpts = new()
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static InstallerWorkerConfig Load()
|
||||||
|
{
|
||||||
|
var path = Path.Combine(Paths.AppDataRoot(), "worker.config.json");
|
||||||
|
if (!File.Exists(path)) return new();
|
||||||
|
var json = File.ReadAllText(path);
|
||||||
|
return JsonSerializer.Deserialize<InstallerWorkerConfig>(json, ReadOpts) ?? new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Save()
|
||||||
|
{
|
||||||
|
var dir = Paths.AppDataRoot();
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
var path = Path.Combine(dir, "worker.config.json");
|
||||||
|
var json = JsonSerializer.Serialize(this, WriteOpts);
|
||||||
|
File.WriteAllText(path, json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mirrors ClaudeDo.Ui.AppSettings JSON shape.
|
||||||
|
/// Keep in sync with src/ClaudeDo.Ui/AppSettings.cs.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class InstallerAppSettings
|
||||||
|
{
|
||||||
|
public string DbPath { get; set; } = "~/.todo-app/todo.db";
|
||||||
|
public string SignalRUrl { get; set; } = "http://127.0.0.1:47821/hub";
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions ReadOpts = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions WriteOpts = new()
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static InstallerAppSettings Load()
|
||||||
|
{
|
||||||
|
var path = Path.Combine(Paths.AppDataRoot(), "ui.config.json");
|
||||||
|
if (!File.Exists(path)) return new();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(path);
|
||||||
|
return JsonSerializer.Deserialize<InstallerAppSettings>(json, ReadOpts) ?? new();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Save()
|
||||||
|
{
|
||||||
|
var dir = Paths.AppDataRoot();
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
var path = Path.Combine(dir, "ui.config.json");
|
||||||
|
var json = JsonSerializer.Serialize(this, WriteOpts);
|
||||||
|
File.WriteAllText(path, json);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/ClaudeDo.Installer/Core/DarkTitleBar.cs
Normal file
29
src/ClaudeDo.Installer/Core/DarkTitleBar.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Interop;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Installer.Core;
|
||||||
|
|
||||||
|
public static class DarkTitleBar
|
||||||
|
{
|
||||||
|
[DllImport("dwmapi.dll", PreserveSig = true)]
|
||||||
|
private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize);
|
||||||
|
|
||||||
|
private const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20;
|
||||||
|
|
||||||
|
public static void Apply(Window window)
|
||||||
|
{
|
||||||
|
if (window.IsLoaded)
|
||||||
|
SetDarkMode(window);
|
||||||
|
else
|
||||||
|
window.SourceInitialized += (_, _) => SetDarkMode(window);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetDarkMode(Window window)
|
||||||
|
{
|
||||||
|
var hwnd = new WindowInteropHelper(window).Handle;
|
||||||
|
if (hwnd == IntPtr.Zero) return;
|
||||||
|
int value = 1;
|
||||||
|
DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, ref value, sizeof(int));
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/ClaudeDo.Installer/Core/IInstallStep.cs
Normal file
20
src/ClaudeDo.Installer/Core/IInstallStep.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
namespace ClaudeDo.Installer.Core;
|
||||||
|
|
||||||
|
public interface IInstallStep
|
||||||
|
{
|
||||||
|
string Name { get; }
|
||||||
|
Task<StepResult> ExecuteAsync(InstallContext ctx, IProgress<string> progress, CancellationToken ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class StepResult
|
||||||
|
{
|
||||||
|
public bool Success { get; init; }
|
||||||
|
public string? ErrorMessage { get; init; }
|
||||||
|
|
||||||
|
public static StepResult Ok() => new() { Success = true };
|
||||||
|
public static StepResult Fail(string error) => new() { Success = false, ErrorMessage = error };
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum StepStatus { Pending, Running, Done, Failed, Skipped }
|
||||||
|
|
||||||
|
public sealed record StepProgress(string StepName, StepStatus Status, string? Message = null);
|
||||||
16
src/ClaudeDo.Installer/Core/IInstallerPage.cs
Normal file
16
src/ClaudeDo.Installer/Core/IInstallerPage.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Installer.Core;
|
||||||
|
|
||||||
|
public interface IInstallerPage
|
||||||
|
{
|
||||||
|
string Title { get; }
|
||||||
|
string Icon { get; }
|
||||||
|
int Order { get; }
|
||||||
|
bool ShowInWizard { get; }
|
||||||
|
bool ShowInSettings { get; }
|
||||||
|
UserControl View { get; }
|
||||||
|
Task LoadAsync();
|
||||||
|
Task ApplyAsync();
|
||||||
|
bool Validate();
|
||||||
|
}
|
||||||
30
src/ClaudeDo.Installer/Core/InstallContext.cs
Normal file
30
src/ClaudeDo.Installer/Core/InstallContext.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
namespace ClaudeDo.Installer.Core;
|
||||||
|
|
||||||
|
public sealed class InstallContext
|
||||||
|
{
|
||||||
|
// WelcomePage
|
||||||
|
public string SourceDirectory { get; set; } = "";
|
||||||
|
public string InstallDirectory { get; set; } = @"C:\Program Files\ClaudeDo";
|
||||||
|
|
||||||
|
// PathsPage
|
||||||
|
public string DbPath { get; set; } = "~/.todo-app/todo.db";
|
||||||
|
public string LogRoot { get; set; } = "~/.todo-app/logs";
|
||||||
|
public string SandboxRoot { get; set; } = "~/.todo-app/sandbox";
|
||||||
|
public string WorktreeRootStrategy { get; set; } = "sibling";
|
||||||
|
public string CentralWorktreeRoot { get; set; } = "~/.todo-app/worktrees";
|
||||||
|
|
||||||
|
// ServicePage
|
||||||
|
public int SignalRPort { get; set; } = 47_821;
|
||||||
|
public int QueueBackstopIntervalMs { get; set; } = 30_000;
|
||||||
|
public string ClaudeBin { get; set; } = "claude";
|
||||||
|
public string ServiceAccount { get; set; } = "LocalSystem";
|
||||||
|
public bool AutoStart { get; set; } = true;
|
||||||
|
public int RestartDelayMs { get; set; } = 5000;
|
||||||
|
|
||||||
|
// UiSettingsPage
|
||||||
|
public string SignalRUrl { get; set; } = "http://127.0.0.1:47821/hub";
|
||||||
|
public string UiDbPath { get; set; } = "~/.todo-app/todo.db";
|
||||||
|
|
||||||
|
// InstallPage
|
||||||
|
public bool CreateDesktopShortcut { get; set; } = true;
|
||||||
|
}
|
||||||
19
src/ClaudeDo.Installer/Core/InstallerMode.cs
Normal file
19
src/ClaudeDo.Installer/Core/InstallerMode.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using System.IO;
|
||||||
|
using ClaudeDo.Data;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Installer.Core;
|
||||||
|
|
||||||
|
public enum InstallerMode { Wizard, Settings }
|
||||||
|
|
||||||
|
public static class ModeDetector
|
||||||
|
{
|
||||||
|
public static InstallerMode Detect()
|
||||||
|
{
|
||||||
|
var root = Paths.AppDataRoot();
|
||||||
|
var workerConfig = Path.Combine(root, "worker.config.json");
|
||||||
|
var uiConfig = Path.Combine(root, "ui.config.json");
|
||||||
|
return File.Exists(workerConfig) && File.Exists(uiConfig)
|
||||||
|
? InstallerMode.Settings
|
||||||
|
: InstallerMode.Wizard;
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/ClaudeDo.Installer/Core/InstallerService.cs
Normal file
49
src/ClaudeDo.Installer/Core/InstallerService.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
namespace ClaudeDo.Installer.Core;
|
||||||
|
|
||||||
|
public sealed class InstallerService
|
||||||
|
{
|
||||||
|
private readonly IEnumerable<IInstallStep> _steps;
|
||||||
|
|
||||||
|
public InstallerService(IEnumerable<IInstallStep> steps) => _steps = steps;
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<(IInstallStep Step, StepResult Result)>> ExecuteAsync(
|
||||||
|
InstallContext ctx,
|
||||||
|
IProgress<StepProgress> progress,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
var results = new List<(IInstallStep, StepResult)>();
|
||||||
|
|
||||||
|
foreach (var step in _steps)
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
progress.Report(new StepProgress(step.Name, StepStatus.Running));
|
||||||
|
|
||||||
|
var lineProgress = new Progress<string>(msg =>
|
||||||
|
progress.Report(new StepProgress(step.Name, StepStatus.Running, msg)));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await step.ExecuteAsync(ctx, lineProgress, ct);
|
||||||
|
var status = result.Success ? StepStatus.Done : StepStatus.Failed;
|
||||||
|
progress.Report(new StepProgress(step.Name, status, result.ErrorMessage));
|
||||||
|
results.Add((step, result));
|
||||||
|
|
||||||
|
if (!result.Success) break;
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
progress.Report(new StepProgress(step.Name, StepStatus.Failed, "Cancelled"));
|
||||||
|
results.Add((step, StepResult.Fail("Cancelled")));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
progress.Report(new StepProgress(step.Name, StepStatus.Failed, ex.Message));
|
||||||
|
results.Add((step, StepResult.Fail(ex.Message)));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/ClaudeDo.Installer/Core/NullToCollapsedConverter.cs
Normal file
14
src/ClaudeDo.Installer/Core/NullToCollapsedConverter.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Data;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Installer.Core;
|
||||||
|
|
||||||
|
public sealed class NullToCollapsedConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
|
=> value is null or "" ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
|
||||||
|
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
|
=> throw new NotSupportedException();
|
||||||
|
}
|
||||||
17
src/ClaudeDo.Installer/Core/PageResolver.cs
Normal file
17
src/ClaudeDo.Installer/Core/PageResolver.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
namespace ClaudeDo.Installer.Core;
|
||||||
|
|
||||||
|
public sealed class PageResolver
|
||||||
|
{
|
||||||
|
private readonly IReadOnlyList<IInstallerPage> _allPages;
|
||||||
|
|
||||||
|
public PageResolver(IEnumerable<IInstallerPage> pages)
|
||||||
|
{
|
||||||
|
_allPages = pages.OrderBy(p => p.Order).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<IInstallerPage> WizardPages =>
|
||||||
|
_allPages.Where(p => p.ShowInWizard).ToList();
|
||||||
|
|
||||||
|
public IReadOnlyList<IInstallerPage> SettingsPages =>
|
||||||
|
_allPages.Where(p => p.ShowInSettings).ToList();
|
||||||
|
}
|
||||||
60
src/ClaudeDo.Installer/Core/ProcessRunner.cs
Normal file
60
src/ClaudeDo.Installer/Core/ProcessRunner.cs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Installer.Core;
|
||||||
|
|
||||||
|
public static class ProcessRunner
|
||||||
|
{
|
||||||
|
public static async Task<(int ExitCode, string Output)> RunAsync(
|
||||||
|
string fileName,
|
||||||
|
string arguments,
|
||||||
|
string? workingDirectory,
|
||||||
|
IProgress<string>? progress,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
var output = new StringBuilder();
|
||||||
|
var outputLock = new object();
|
||||||
|
|
||||||
|
using var process = new Process();
|
||||||
|
process.StartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = fileName,
|
||||||
|
Arguments = arguments,
|
||||||
|
WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!process.Start())
|
||||||
|
return (-1, "Failed to start process");
|
||||||
|
|
||||||
|
var stdoutTask = ReadStreamAsync(process.StandardOutput, output, outputLock, progress);
|
||||||
|
var stderrTask = ReadStreamAsync(process.StandardError, output, outputLock, progress);
|
||||||
|
|
||||||
|
using var reg = ct.Register(() =>
|
||||||
|
{
|
||||||
|
try { process.Kill(entireProcessTree: true); } catch { }
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.WhenAll(stdoutTask, stderrTask);
|
||||||
|
await process.WaitForExitAsync(ct);
|
||||||
|
|
||||||
|
return (process.ExitCode, output.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task ReadStreamAsync(
|
||||||
|
StreamReader reader,
|
||||||
|
StringBuilder output,
|
||||||
|
object outputLock,
|
||||||
|
IProgress<string>? progress)
|
||||||
|
{
|
||||||
|
while (await reader.ReadLineAsync() is { } line)
|
||||||
|
{
|
||||||
|
lock (outputLock) { output.AppendLine(line); }
|
||||||
|
progress?.Report(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/ClaudeDo.Installer/Core/StepIndicatorConverter.cs
Normal file
37
src/ClaudeDo.Installer/Core/StepIndicatorConverter.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Data;
|
||||||
|
using System.Windows.Media;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Installer.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Multi-value converter: compares the page's index with the current page index
|
||||||
|
/// to determine step indicator styling.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StepActiveConverter : IMultiValueConverter
|
||||||
|
{
|
||||||
|
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
if (values.Length < 2 ||
|
||||||
|
values[0] is not IInstallerPage page ||
|
||||||
|
values[1] is not IInstallerPage currentPage)
|
||||||
|
return DependencyProperty.UnsetValue;
|
||||||
|
|
||||||
|
var isActive = ReferenceEquals(page, currentPage);
|
||||||
|
|
||||||
|
var key = parameter?.ToString() switch
|
||||||
|
{
|
||||||
|
"Background" => isActive ? "AccentBrush" : "WindowBgBrush",
|
||||||
|
"Foreground" => isActive ? "TextPrimaryBrush" : "TextMutedBrush",
|
||||||
|
"BorderBrush" => isActive ? "AccentBrush" : "BorderSubtleBrush",
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (key is null) return DependencyProperty.UnsetValue;
|
||||||
|
return Application.Current.Resources[key] as SolidColorBrush ?? DependencyProperty.UnsetValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
||||||
|
=> throw new NotSupportedException();
|
||||||
|
}
|
||||||
101
src/ClaudeDo.Installer/Pages/InstallPage/InstallPageView.xaml
Normal file
101
src/ClaudeDo.Installer/Pages/InstallPage/InstallPageView.xaml
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<UserControl x:Class="ClaudeDo.Installer.Pages.InstallPage.InstallPageView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:local="clr-namespace:ClaudeDo.Installer.Pages.InstallPage"
|
||||||
|
d:DataContext="{d:DesignInstance local:InstallPageViewModel}"
|
||||||
|
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"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<StackPanel Grid.Row="0" Margin="0,0,0,16">
|
||||||
|
<TextBlock Text="Installation" FontSize="18" FontWeight="SemiBold" Margin="0,0,0,4"/>
|
||||||
|
<TextBlock Text="Click Install to build and deploy ClaudeDo."
|
||||||
|
Foreground="{StaticResource TextSecondaryBrush}" TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Step List -->
|
||||||
|
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
|
||||||
|
<ItemsControl ItemsSource="{Binding Steps}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate DataType="{x:Type local:StepViewModel}">
|
||||||
|
<Border Margin="0,0,0,6" Padding="10,8"
|
||||||
|
Background="{StaticResource IslandBgBrush}"
|
||||||
|
BorderBrush="{StaticResource BorderSubtleBrush}"
|
||||||
|
BorderThickness="1" CornerRadius="4">
|
||||||
|
<StackPanel>
|
||||||
|
<!-- Step header -->
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- Status indicator -->
|
||||||
|
<Ellipse Grid.Column="0" Width="10" Height="10" Margin="0,0,10,0"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<Ellipse.Style>
|
||||||
|
<Style TargetType="Ellipse">
|
||||||
|
<Setter Property="Fill" Value="{StaticResource StatusGrayBrush}"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding Status}" Value="Running">
|
||||||
|
<Setter Property="Fill" Value="{StaticResource StatusOrangeBrush}"/>
|
||||||
|
</DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding Status}" Value="Done">
|
||||||
|
<Setter Property="Fill" Value="{StaticResource StatusGreenBrush}"/>
|
||||||
|
</DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding Status}" Value="Failed">
|
||||||
|
<Setter Property="Fill" Value="{StaticResource StatusRedBrush}"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Ellipse.Style>
|
||||||
|
</Ellipse>
|
||||||
|
|
||||||
|
<TextBlock Grid.Column="1" Text="{Binding Name}" FontSize="13"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Messages (expandable) -->
|
||||||
|
<ItemsControl ItemsSource="{Binding Messages}" Margin="20,4,0,0"
|
||||||
|
Visibility="{Binding IsExpanded, Converter={StaticResource BoolToVisConverter}}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{Binding}" FontSize="11" FontFamily="Consolas"
|
||||||
|
Foreground="{StaticResource TextDimBrush}"
|
||||||
|
TextWrapping="Wrap"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<!-- Progress Bar -->
|
||||||
|
<ProgressBar Grid.Row="2" Value="{Binding OverallProgress}" Maximum="100"
|
||||||
|
Margin="0,12,0,0"
|
||||||
|
Visibility="{Binding IsInstalling, Converter={StaticResource BoolToVisConverter}}"/>
|
||||||
|
|
||||||
|
<!-- Action buttons -->
|
||||||
|
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
|
||||||
|
<Button Content="Cancel" Command="{Binding CancelInstallCommand}"
|
||||||
|
Visibility="{Binding IsInstalling, Converter={StaticResource BoolToVisConverter}}"
|
||||||
|
Margin="0,0,8,0"/>
|
||||||
|
|
||||||
|
<Button Content="Launch ClaudeDo" Command="{Binding LaunchAppCommand}"
|
||||||
|
Style="{StaticResource AccentButton}"
|
||||||
|
Visibility="{Binding IsComplete, Converter={StaticResource BoolToVisConverter}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Installer.Pages.InstallPage;
|
||||||
|
|
||||||
|
public partial class InstallPageView : UserControl
|
||||||
|
{
|
||||||
|
public InstallPageView() => InitializeComponent();
|
||||||
|
}
|
||||||
120
src/ClaudeDo.Installer/Pages/InstallPage/InstallPageViewModel.cs
Normal file
120
src/ClaudeDo.Installer/Pages/InstallPage/InstallPageViewModel.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/ClaudeDo.Installer/Pages/InstallPage/StepViewModel.cs
Normal file
17
src/ClaudeDo.Installer/Pages/InstallPage/StepViewModel.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using ClaudeDo.Installer.Core;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
41
src/ClaudeDo.Installer/Pages/PathsPage/PathsPageView.xaml
Normal file
41
src/ClaudeDo.Installer/Pages/PathsPage/PathsPageView.xaml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<UserControl x:Class="ClaudeDo.Installer.Pages.PathsPage.PathsPageView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:local="clr-namespace:ClaudeDo.Installer.Pages.PathsPage"
|
||||||
|
d:DataContext="{d:DesignInstance local:PathsPageViewModel}"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel MaxWidth="520">
|
||||||
|
<TextBlock Text="Data Paths" FontSize="18" FontWeight="SemiBold" Margin="0,0,0,4"/>
|
||||||
|
<TextBlock Text="Configure where ClaudeDo stores its data."
|
||||||
|
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,20"
|
||||||
|
TextWrapping="Wrap"/>
|
||||||
|
|
||||||
|
<Label Content="Database Path"/>
|
||||||
|
<TextBox Text="{Binding DbPath, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,12"/>
|
||||||
|
|
||||||
|
<Label Content="Log Directory"/>
|
||||||
|
<TextBox Text="{Binding LogRoot, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,12"/>
|
||||||
|
|
||||||
|
<Label Content="Sandbox Root"/>
|
||||||
|
<TextBox Text="{Binding SandboxRoot, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,12"/>
|
||||||
|
|
||||||
|
<Label Content="Worktree Strategy"/>
|
||||||
|
<ComboBox SelectedItem="{Binding WorktreeRootStrategy}" Margin="0,0,0,12">
|
||||||
|
<sys:String xmlns:sys="clr-namespace:System;assembly=mscorlib">sibling</sys:String>
|
||||||
|
<sys:String xmlns:sys="clr-namespace:System;assembly=mscorlib">central</sys:String>
|
||||||
|
</ComboBox>
|
||||||
|
|
||||||
|
<StackPanel Visibility="{Binding IsCentralVisible, Converter={StaticResource BoolToVisConverter}}">
|
||||||
|
<Label Content="Central Worktree Root"/>
|
||||||
|
<TextBox Text="{Binding CentralWorktreeRoot, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,12"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Text="{Binding ValidationError}" Foreground="{StaticResource ErrorBrush}" FontSize="11"
|
||||||
|
Visibility="{Binding ValidationError, Converter={StaticResource NullToCollapsedConverter}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Installer.Pages.PathsPage;
|
||||||
|
|
||||||
|
public partial class PathsPageView : UserControl
|
||||||
|
{
|
||||||
|
public PathsPageView() => InitializeComponent();
|
||||||
|
}
|
||||||
74
src/ClaudeDo.Installer/Pages/PathsPage/PathsPageViewModel.cs
Normal file
74
src/ClaudeDo.Installer/Pages/PathsPage/PathsPageViewModel.cs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
using System.Windows.Controls;
|
||||||
|
using ClaudeDo.Installer.Core;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Installer.Pages.PathsPage;
|
||||||
|
|
||||||
|
public partial class PathsPageViewModel : ObservableObject, IInstallerPage
|
||||||
|
{
|
||||||
|
private readonly InstallContext _context;
|
||||||
|
private PathsPageView? _view;
|
||||||
|
|
||||||
|
public string Title => "Paths";
|
||||||
|
public string Icon => "\uE8B7";
|
||||||
|
public int Order => 1;
|
||||||
|
public bool ShowInWizard => true;
|
||||||
|
public bool ShowInSettings => true;
|
||||||
|
public UserControl View => _view ??= new PathsPageView { DataContext = this };
|
||||||
|
|
||||||
|
[ObservableProperty] private string _dbPath = "~/.todo-app/todo.db";
|
||||||
|
[ObservableProperty] private string _logRoot = "~/.todo-app/logs";
|
||||||
|
[ObservableProperty] private string _sandboxRoot = "~/.todo-app/sandbox";
|
||||||
|
[ObservableProperty] private string _worktreeRootStrategy = "sibling";
|
||||||
|
[ObservableProperty] private string _centralWorktreeRoot = "~/.todo-app/worktrees";
|
||||||
|
[ObservableProperty] private string? _validationError;
|
||||||
|
|
||||||
|
public bool IsCentralVisible => WorktreeRootStrategy == "central";
|
||||||
|
|
||||||
|
public PathsPageViewModel(InstallContext context) => _context = context;
|
||||||
|
|
||||||
|
partial void OnWorktreeRootStrategyChanged(string value) =>
|
||||||
|
OnPropertyChanged(nameof(IsCentralVisible));
|
||||||
|
|
||||||
|
public Task LoadAsync()
|
||||||
|
{
|
||||||
|
var cfg = InstallerWorkerConfig.Load();
|
||||||
|
DbPath = cfg.DbPath;
|
||||||
|
LogRoot = cfg.LogRoot;
|
||||||
|
SandboxRoot = cfg.SandboxRoot;
|
||||||
|
WorktreeRootStrategy = cfg.WorktreeRootStrategy;
|
||||||
|
CentralWorktreeRoot = cfg.CentralWorktreeRoot;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ApplyAsync()
|
||||||
|
{
|
||||||
|
_context.DbPath = DbPath;
|
||||||
|
_context.UiDbPath = DbPath;
|
||||||
|
_context.LogRoot = LogRoot;
|
||||||
|
_context.SandboxRoot = SandboxRoot;
|
||||||
|
_context.WorktreeRootStrategy = WorktreeRootStrategy;
|
||||||
|
_context.CentralWorktreeRoot = CentralWorktreeRoot;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Validate()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(DbPath) ||
|
||||||
|
string.IsNullOrWhiteSpace(LogRoot) ||
|
||||||
|
string.IsNullOrWhiteSpace(SandboxRoot))
|
||||||
|
{
|
||||||
|
ValidationError = "All path fields are required.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WorktreeRootStrategy == "central" && string.IsNullOrWhiteSpace(CentralWorktreeRoot))
|
||||||
|
{
|
||||||
|
ValidationError = "Central worktree root is required when using central strategy.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidationError = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<UserControl x:Class="ClaudeDo.Installer.Pages.ServicePage.ServicePageView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:local="clr-namespace:ClaudeDo.Installer.Pages.ServicePage"
|
||||||
|
d:DataContext="{d:DesignInstance local:ServicePageViewModel}"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel MaxWidth="520">
|
||||||
|
<TextBlock Text="Worker Service" FontSize="18" FontWeight="SemiBold" Margin="0,0,0,4"/>
|
||||||
|
<TextBlock Text="Configure the ClaudeDo Worker background service."
|
||||||
|
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,20"
|
||||||
|
TextWrapping="Wrap"/>
|
||||||
|
|
||||||
|
<Label Content="SignalR Port"/>
|
||||||
|
<TextBox Text="{Binding SignalRPort, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,12"/>
|
||||||
|
|
||||||
|
<Label Content="Queue Backstop Interval (ms)"/>
|
||||||
|
<TextBox Text="{Binding QueueBackstopIntervalMs, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,12"/>
|
||||||
|
|
||||||
|
<Label Content="Claude CLI Path"/>
|
||||||
|
<Grid Margin="0,0,0,12">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBox Grid.Column="0" Text="{Binding ClaudeBin, UpdateSourceTrigger=PropertyChanged}"/>
|
||||||
|
<Button Grid.Column="1" Content="Browse..." Command="{Binding BrowseClaudeCommand}"
|
||||||
|
Margin="8,0,0,0"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Separator Margin="0,4,0,12"/>
|
||||||
|
|
||||||
|
<Label Content="Service Account"/>
|
||||||
|
<StackPanel Margin="0,0,0,12">
|
||||||
|
<RadioButton Content="Local System (recommended)"
|
||||||
|
IsChecked="{Binding IsLocalSystem}" Margin="0,0,0,4"/>
|
||||||
|
<RadioButton Content="Current User"
|
||||||
|
IsChecked="{Binding IsCurrentUser}"/>
|
||||||
|
<TextBlock Text="Running as current user requires 'Log on as a service' privilege."
|
||||||
|
Foreground="{StaticResource TextDimBrush}" FontSize="11" Margin="20,2,0,0"
|
||||||
|
TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<CheckBox Content="Start service automatically" IsChecked="{Binding AutoStart}" Margin="0,0,0,12"/>
|
||||||
|
|
||||||
|
<Label Content="Restart Delay (ms)"/>
|
||||||
|
<TextBox Text="{Binding RestartDelayMs, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,12"/>
|
||||||
|
|
||||||
|
<TextBlock Text="{Binding ValidationError}" Foreground="{StaticResource ErrorBrush}" FontSize="11"
|
||||||
|
Visibility="{Binding ValidationError, Converter={StaticResource NullToCollapsedConverter}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Installer.Pages.ServicePage;
|
||||||
|
|
||||||
|
public partial class ServicePageView : UserControl
|
||||||
|
{
|
||||||
|
public ServicePageView() => InitializeComponent();
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
using System.Windows.Controls;
|
||||||
|
using ClaudeDo.Installer.Core;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Installer.Pages.ServicePage;
|
||||||
|
|
||||||
|
public partial class ServicePageViewModel : ObservableObject, IInstallerPage
|
||||||
|
{
|
||||||
|
private readonly InstallContext _context;
|
||||||
|
private ServicePageView? _view;
|
||||||
|
|
||||||
|
public string Title => "Service";
|
||||||
|
public string Icon => "\uE912";
|
||||||
|
public int Order => 2;
|
||||||
|
public bool ShowInWizard => true;
|
||||||
|
public bool ShowInSettings => true;
|
||||||
|
public UserControl View => _view ??= new ServicePageView { DataContext = this };
|
||||||
|
|
||||||
|
[ObservableProperty] private int _signalRPort = 47_821;
|
||||||
|
[ObservableProperty] private int _queueBackstopIntervalMs = 30_000;
|
||||||
|
[ObservableProperty] private string _claudeBin = "claude";
|
||||||
|
[ObservableProperty] private bool _isLocalSystem = true;
|
||||||
|
[ObservableProperty] private bool _isCurrentUser;
|
||||||
|
[ObservableProperty] private bool _autoStart = true;
|
||||||
|
[ObservableProperty] private int _restartDelayMs = 5000;
|
||||||
|
[ObservableProperty] private string? _validationError;
|
||||||
|
|
||||||
|
public ServicePageViewModel(InstallContext context) => _context = context;
|
||||||
|
|
||||||
|
public Task LoadAsync()
|
||||||
|
{
|
||||||
|
var cfg = InstallerWorkerConfig.Load();
|
||||||
|
SignalRPort = cfg.SignalRPort;
|
||||||
|
QueueBackstopIntervalMs = cfg.QueueBackstopIntervalMs;
|
||||||
|
ClaudeBin = cfg.ClaudeBin;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ApplyAsync()
|
||||||
|
{
|
||||||
|
_context.SignalRPort = SignalRPort;
|
||||||
|
_context.QueueBackstopIntervalMs = QueueBackstopIntervalMs;
|
||||||
|
_context.ClaudeBin = ClaudeBin;
|
||||||
|
_context.ServiceAccount = IsCurrentUser ? "CurrentUser" : "LocalSystem";
|
||||||
|
_context.AutoStart = AutoStart;
|
||||||
|
_context.RestartDelayMs = RestartDelayMs;
|
||||||
|
_context.SignalRUrl = $"http://127.0.0.1:{SignalRPort}/hub";
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Validate()
|
||||||
|
{
|
||||||
|
if (SignalRPort < 1024 || SignalRPort > 65535)
|
||||||
|
{
|
||||||
|
ValidationError = "Port must be between 1024 and 65535.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (QueueBackstopIntervalMs <= 0)
|
||||||
|
{
|
||||||
|
ValidationError = "Queue backstop interval must be greater than 0.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(ClaudeBin))
|
||||||
|
{
|
||||||
|
ValidationError = "Claude CLI path is required.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidationError = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void BrowseClaude()
|
||||||
|
{
|
||||||
|
var dialog = new OpenFileDialog
|
||||||
|
{
|
||||||
|
Title = "Select Claude CLI executable",
|
||||||
|
Filter = "Executables (*.exe)|*.exe|All files (*.*)|*.*",
|
||||||
|
};
|
||||||
|
if (dialog.ShowDialog() == true)
|
||||||
|
ClaudeBin = dialog.FileName;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<UserControl x:Class="ClaudeDo.Installer.Pages.UiSettingsPage.UiSettingsPageView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:local="clr-namespace:ClaudeDo.Installer.Pages.UiSettingsPage"
|
||||||
|
d:DataContext="{d:DesignInstance local:UiSettingsPageViewModel}"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel MaxWidth="520">
|
||||||
|
<TextBlock Text="UI Settings" FontSize="18" FontWeight="SemiBold" Margin="0,0,0,4"/>
|
||||||
|
<TextBlock Text="Configure the ClaudeDo desktop UI connection settings."
|
||||||
|
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,20"
|
||||||
|
TextWrapping="Wrap"/>
|
||||||
|
|
||||||
|
<CheckBox Content="Sync with service settings" IsChecked="{Binding IsSynced}" Margin="0,0,0,16"/>
|
||||||
|
|
||||||
|
<Label Content="SignalR URL"/>
|
||||||
|
<TextBox Text="{Binding SignalRUrl, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
IsReadOnly="{Binding IsSynced}" Margin="0,0,0,12"/>
|
||||||
|
|
||||||
|
<Label Content="Database Path"/>
|
||||||
|
<TextBox Text="{Binding UiDbPath, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
IsReadOnly="{Binding IsSynced}" Margin="0,0,0,12"/>
|
||||||
|
|
||||||
|
<TextBlock Text="When synced, these values are derived from the Service and Paths pages."
|
||||||
|
Foreground="{StaticResource TextDimBrush}" FontSize="11" TextWrapping="Wrap"
|
||||||
|
Visibility="{Binding IsSynced, Converter={StaticResource BoolToVisConverter}}"
|
||||||
|
Margin="0,0,0,12"/>
|
||||||
|
|
||||||
|
<TextBlock Text="{Binding ValidationError}" Foreground="{StaticResource ErrorBrush}" FontSize="11"
|
||||||
|
Visibility="{Binding ValidationError, Converter={StaticResource NullToCollapsedConverter}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Installer.Pages.UiSettingsPage;
|
||||||
|
|
||||||
|
public partial class UiSettingsPageView : UserControl
|
||||||
|
{
|
||||||
|
public UiSettingsPageView() => InitializeComponent();
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
using System.Windows.Controls;
|
||||||
|
using ClaudeDo.Installer.Core;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Installer.Pages.UiSettingsPage;
|
||||||
|
|
||||||
|
public partial class UiSettingsPageViewModel : ObservableObject, IInstallerPage
|
||||||
|
{
|
||||||
|
private readonly InstallContext _context;
|
||||||
|
private UiSettingsPageView? _view;
|
||||||
|
|
||||||
|
public string Title => "UI Settings";
|
||||||
|
public string Icon => "\uE771";
|
||||||
|
public int Order => 3;
|
||||||
|
public bool ShowInWizard => true;
|
||||||
|
public bool ShowInSettings => true;
|
||||||
|
public UserControl View => _view ??= new UiSettingsPageView { DataContext = this };
|
||||||
|
|
||||||
|
[ObservableProperty] private string _signalRUrl = "http://127.0.0.1:47821/hub";
|
||||||
|
[ObservableProperty] private string _uiDbPath = "~/.todo-app/todo.db";
|
||||||
|
[ObservableProperty] private bool _isSynced = true;
|
||||||
|
[ObservableProperty] private string? _validationError;
|
||||||
|
|
||||||
|
public UiSettingsPageViewModel(InstallContext context) => _context = context;
|
||||||
|
|
||||||
|
partial void OnIsSyncedChanged(bool value)
|
||||||
|
{
|
||||||
|
if (value) SyncFromContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SyncFromContext()
|
||||||
|
{
|
||||||
|
SignalRUrl = $"http://127.0.0.1:{_context.SignalRPort}/hub";
|
||||||
|
UiDbPath = _context.DbPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task LoadAsync()
|
||||||
|
{
|
||||||
|
if (IsSynced)
|
||||||
|
{
|
||||||
|
SyncFromContext();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var cfg = InstallerAppSettings.Load();
|
||||||
|
SignalRUrl = cfg.SignalRUrl;
|
||||||
|
UiDbPath = cfg.DbPath;
|
||||||
|
}
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ApplyAsync()
|
||||||
|
{
|
||||||
|
if (IsSynced) SyncFromContext();
|
||||||
|
_context.SignalRUrl = SignalRUrl;
|
||||||
|
_context.UiDbPath = UiDbPath;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Validate()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(SignalRUrl))
|
||||||
|
{
|
||||||
|
ValidationError = "SignalR URL is required.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(UiDbPath))
|
||||||
|
{
|
||||||
|
ValidationError = "Database path is required.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Uri.TryCreate(SignalRUrl, UriKind.Absolute, out _))
|
||||||
|
{
|
||||||
|
ValidationError = "SignalR URL must be a valid URL.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidationError = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<UserControl x:Class="ClaudeDo.Installer.Pages.WelcomePage.WelcomePageView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:local="clr-namespace:ClaudeDo.Installer.Pages.WelcomePage"
|
||||||
|
d:DataContext="{d:DesignInstance local:WelcomePageViewModel}"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel MaxWidth="520">
|
||||||
|
<TextBlock Text="Welcome to ClaudeDo Setup" FontSize="20" FontWeight="SemiBold"
|
||||||
|
Margin="0,0,0,6"/>
|
||||||
|
<TextBlock Text="This wizard will build, configure, and install ClaudeDo on your machine."
|
||||||
|
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,24"
|
||||||
|
TextWrapping="Wrap"/>
|
||||||
|
|
||||||
|
<!-- Source Directory -->
|
||||||
|
<Label Content="Source Directory"/>
|
||||||
|
<Grid Margin="0,0,0,4">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBox Grid.Column="0" Text="{Binding SourceDirectory, UpdateSourceTrigger=PropertyChanged}"/>
|
||||||
|
<Button Grid.Column="1" Content="Browse..." Command="{Binding BrowseSourceCommand}"
|
||||||
|
Margin="8,0,0,0"/>
|
||||||
|
</Grid>
|
||||||
|
<TextBlock Text="{Binding SourceError}" Foreground="{StaticResource ErrorBrush}" FontSize="11"
|
||||||
|
Visibility="{Binding SourceError, Converter={StaticResource NullToCollapsedConverter}}"
|
||||||
|
Margin="0,0,0,16"/>
|
||||||
|
|
||||||
|
<!-- Install Directory -->
|
||||||
|
<Label Content="Install Directory"/>
|
||||||
|
<Grid Margin="0,0,0,4">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBox Grid.Column="0" Text="{Binding InstallDirectory, UpdateSourceTrigger=PropertyChanged}"/>
|
||||||
|
<Button Grid.Column="1" Content="Browse..." Command="{Binding BrowseInstallCommand}"
|
||||||
|
Margin="8,0,0,0"/>
|
||||||
|
</Grid>
|
||||||
|
<TextBlock Text="{Binding InstallError}" Foreground="{StaticResource ErrorBrush}" FontSize="11"
|
||||||
|
Visibility="{Binding InstallError, Converter={StaticResource NullToCollapsedConverter}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Installer.Pages.WelcomePage;
|
||||||
|
|
||||||
|
public partial class WelcomePageView : UserControl
|
||||||
|
{
|
||||||
|
public WelcomePageView() => InitializeComponent();
|
||||||
|
}
|
||||||
106
src/ClaudeDo.Installer/Pages/WelcomePage/WelcomePageViewModel.cs
Normal file
106
src/ClaudeDo.Installer/Pages/WelcomePage/WelcomePageViewModel.cs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using ClaudeDo.Installer.Core;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Installer.Pages.WelcomePage;
|
||||||
|
|
||||||
|
public partial class WelcomePageViewModel : ObservableObject, IInstallerPage
|
||||||
|
{
|
||||||
|
private readonly InstallContext _context;
|
||||||
|
private WelcomePageView? _view;
|
||||||
|
|
||||||
|
public string Title => "Welcome";
|
||||||
|
public string Icon => "\uE80F";
|
||||||
|
public int Order => 0;
|
||||||
|
public bool ShowInWizard => true;
|
||||||
|
public bool ShowInSettings => false;
|
||||||
|
public UserControl View => _view ??= new WelcomePageView { DataContext = this };
|
||||||
|
|
||||||
|
[ObservableProperty] private string _sourceDirectory = "";
|
||||||
|
[ObservableProperty] private string _installDirectory = @"C:\Program Files\ClaudeDo";
|
||||||
|
[ObservableProperty] private string? _sourceError;
|
||||||
|
[ObservableProperty] private string? _installError;
|
||||||
|
|
||||||
|
public WelcomePageViewModel(InstallContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_sourceDirectory = DetectSourceDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task LoadAsync()
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(_context.SourceDirectory))
|
||||||
|
SourceDirectory = _context.SourceDirectory;
|
||||||
|
if (!string.IsNullOrEmpty(_context.InstallDirectory))
|
||||||
|
InstallDirectory = _context.InstallDirectory;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ApplyAsync()
|
||||||
|
{
|
||||||
|
_context.SourceDirectory = SourceDirectory;
|
||||||
|
_context.InstallDirectory = InstallDirectory;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Validate()
|
||||||
|
{
|
||||||
|
var valid = true;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(SourceDirectory) ||
|
||||||
|
!File.Exists(Path.Combine(SourceDirectory, "ClaudeDo.slnx")))
|
||||||
|
{
|
||||||
|
SourceError = "Source directory must contain ClaudeDo.slnx";
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SourceError = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(InstallDirectory))
|
||||||
|
{
|
||||||
|
InstallError = "Install directory is required";
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
InstallError = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void BrowseSource()
|
||||||
|
{
|
||||||
|
var dialog = new OpenFolderDialog { Title = "Select ClaudeDo source directory" };
|
||||||
|
if (dialog.ShowDialog() == true)
|
||||||
|
SourceDirectory = dialog.FolderName;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void BrowseInstall()
|
||||||
|
{
|
||||||
|
var dialog = new OpenFolderDialog { Title = "Select installation directory" };
|
||||||
|
if (dialog.ShowDialog() == true)
|
||||||
|
InstallDirectory = dialog.FolderName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string DetectSourceDirectory()
|
||||||
|
{
|
||||||
|
var dir = AppContext.BaseDirectory;
|
||||||
|
for (var i = 0; i < 8; i++)
|
||||||
|
{
|
||||||
|
if (File.Exists(Path.Combine(dir, "ClaudeDo.slnx")))
|
||||||
|
return dir;
|
||||||
|
var parent = Directory.GetParent(dir)?.FullName;
|
||||||
|
if (parent is null) break;
|
||||||
|
dir = parent;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
91
src/ClaudeDo.Installer/Steps/CreateShortcutsStep.cs
Normal file
91
src/ClaudeDo.Installer/Steps/CreateShortcutsStep.cs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Runtime.InteropServices.ComTypes;
|
||||||
|
using System.Text;
|
||||||
|
using ClaudeDo.Installer.Core;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Installer.Steps;
|
||||||
|
|
||||||
|
public sealed class CreateShortcutsStep : IInstallStep
|
||||||
|
{
|
||||||
|
public string Name => "Create Shortcuts";
|
||||||
|
|
||||||
|
public Task<StepResult> ExecuteAsync(InstallContext ctx, IProgress<string> progress, CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var appExe = Path.Combine(ctx.InstallDirectory, "app", "ClaudeDo.App.exe");
|
||||||
|
var workingDir = Path.Combine(ctx.InstallDirectory, "app");
|
||||||
|
|
||||||
|
// Start Menu shortcut
|
||||||
|
var startMenuDir = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu),
|
||||||
|
"Programs");
|
||||||
|
Directory.CreateDirectory(startMenuDir);
|
||||||
|
var startMenuPath = Path.Combine(startMenuDir, "ClaudeDo.lnk");
|
||||||
|
CreateShortcut(startMenuPath, appExe, workingDir, "ClaudeDo Task Manager");
|
||||||
|
progress.Report($"Created Start Menu shortcut: {startMenuPath}");
|
||||||
|
|
||||||
|
// Desktop shortcut (optional)
|
||||||
|
if (ctx.CreateDesktopShortcut)
|
||||||
|
{
|
||||||
|
var desktopPath = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory),
|
||||||
|
"ClaudeDo.lnk");
|
||||||
|
CreateShortcut(desktopPath, appExe, workingDir, "ClaudeDo Task Manager");
|
||||||
|
progress.Report($"Created Desktop shortcut: {desktopPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(StepResult.Ok());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Task.FromResult(StepResult.Fail(ex.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CreateShortcut(string shortcutPath, string targetPath, string workingDir, string description)
|
||||||
|
{
|
||||||
|
var link = (IShellLink)new ShellLink();
|
||||||
|
link.SetPath(targetPath);
|
||||||
|
link.SetWorkingDirectory(workingDir);
|
||||||
|
link.SetDescription(description);
|
||||||
|
link.SetIconLocation(targetPath, 0);
|
||||||
|
|
||||||
|
var file = (IPersistFile)link;
|
||||||
|
file.Save(shortcutPath, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region COM Interop for IShellLink
|
||||||
|
|
||||||
|
[ComImport]
|
||||||
|
[Guid("00021401-0000-0000-C000-000000000046")]
|
||||||
|
private class ShellLink { }
|
||||||
|
|
||||||
|
[ComImport]
|
||||||
|
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||||
|
[Guid("000214F9-0000-0000-C000-000000000046")]
|
||||||
|
private interface IShellLink
|
||||||
|
{
|
||||||
|
void GetPath([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, IntPtr pfd, int fFlags);
|
||||||
|
void GetIDList(out IntPtr ppidl);
|
||||||
|
void SetIDList(IntPtr pidl);
|
||||||
|
void GetDescription([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName);
|
||||||
|
void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
|
||||||
|
void GetWorkingDirectory([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath);
|
||||||
|
void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
|
||||||
|
void GetArguments([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath);
|
||||||
|
void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
|
||||||
|
void GetHotkey(out short pwHotkey);
|
||||||
|
void SetHotkey(short wHotkey);
|
||||||
|
void GetShowCmd(out int piShowCmd);
|
||||||
|
void SetShowCmd(int iShowCmd);
|
||||||
|
void GetIconLocation([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath, int cchIconPath, out int piIcon);
|
||||||
|
void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
|
||||||
|
void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved);
|
||||||
|
void Resolve(IntPtr hwnd, int fFlags);
|
||||||
|
void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
62
src/ClaudeDo.Installer/Steps/DeployBinariesStep.cs
Normal file
62
src/ClaudeDo.Installer/Steps/DeployBinariesStep.cs
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
using System.IO;
|
||||||
|
using ClaudeDo.Installer.Core;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Installer.Steps;
|
||||||
|
|
||||||
|
public sealed class DeployBinariesStep : IInstallStep
|
||||||
|
{
|
||||||
|
public string Name => "Deploy Binaries";
|
||||||
|
|
||||||
|
public Task<StepResult> ExecuteAsync(InstallContext ctx, IProgress<string> progress, CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var appPublish = Path.Combine(ctx.SourceDirectory, "src", "ClaudeDo.App", "bin", "Release", "net8.0", "win-x64", "publish");
|
||||||
|
var workerPublish = Path.Combine(ctx.SourceDirectory, "src", "ClaudeDo.Worker", "bin", "Release", "net8.0", "win-x64", "publish");
|
||||||
|
|
||||||
|
var appDest = Path.Combine(ctx.InstallDirectory, "app");
|
||||||
|
var workerDest = Path.Combine(ctx.InstallDirectory, "worker");
|
||||||
|
|
||||||
|
if (!Directory.Exists(appPublish))
|
||||||
|
return Task.FromResult(StepResult.Fail($"App publish directory not found: {appPublish}"));
|
||||||
|
if (!Directory.Exists(workerPublish))
|
||||||
|
return Task.FromResult(StepResult.Fail($"Worker publish directory not found: {workerPublish}"));
|
||||||
|
|
||||||
|
var appCount = CopyDirectory(appPublish, appDest, progress, ct);
|
||||||
|
progress.Report($"Copied {appCount} files to {appDest}");
|
||||||
|
|
||||||
|
var workerCount = CopyDirectory(workerPublish, workerDest, progress, ct);
|
||||||
|
progress.Report($"Copied {workerCount} files to {workerDest}");
|
||||||
|
|
||||||
|
return Task.FromResult(StepResult.Ok());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Task.FromResult(StepResult.Fail(ex.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CopyDirectory(string source, string dest, IProgress<string> progress, CancellationToken ct)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(dest);
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
foreach (var dir in Directory.GetDirectories(source, "*", SearchOption.AllDirectories))
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
var relative = Path.GetRelativePath(source, dir);
|
||||||
|
Directory.CreateDirectory(Path.Combine(dest, relative));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var file in Directory.GetFiles(source, "*", SearchOption.AllDirectories))
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
var relative = Path.GetRelativePath(source, file);
|
||||||
|
var destFile = Path.Combine(dest, relative);
|
||||||
|
File.Copy(file, destFile, overwrite: true);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/ClaudeDo.Installer/Steps/InitDatabaseStep.cs
Normal file
28
src/ClaudeDo.Installer/Steps/InitDatabaseStep.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using ClaudeDo.Data;
|
||||||
|
using ClaudeDo.Installer.Core;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Installer.Steps;
|
||||||
|
|
||||||
|
public sealed class InitDatabaseStep : IInstallStep
|
||||||
|
{
|
||||||
|
public string Name => "Initialize Database";
|
||||||
|
|
||||||
|
public Task<StepResult> ExecuteAsync(InstallContext ctx, IProgress<string> progress, CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var expandedPath = Paths.Expand(ctx.DbPath);
|
||||||
|
progress.Report($"Initializing database at {expandedPath}");
|
||||||
|
|
||||||
|
var factory = new SqliteConnectionFactory(expandedPath);
|
||||||
|
SchemaInitializer.Apply(factory);
|
||||||
|
|
||||||
|
progress.Report("Schema applied successfully");
|
||||||
|
return Task.FromResult(StepResult.Ok());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Task.FromResult(StepResult.Fail(ex.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/ClaudeDo.Installer/Steps/PublishAppStep.cs
Normal file
20
src/ClaudeDo.Installer/Steps/PublishAppStep.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using ClaudeDo.Installer.Core;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Installer.Steps;
|
||||||
|
|
||||||
|
public sealed class PublishAppStep : IInstallStep
|
||||||
|
{
|
||||||
|
public string Name => "Publish ClaudeDo.App";
|
||||||
|
|
||||||
|
public async Task<StepResult> ExecuteAsync(InstallContext ctx, IProgress<string> progress, CancellationToken ct)
|
||||||
|
{
|
||||||
|
progress.Report("Publishing ClaudeDo.App...");
|
||||||
|
|
||||||
|
var args = "publish src/ClaudeDo.App/ClaudeDo.App.csproj -c Release -r win-x64 --self-contained false";
|
||||||
|
var (exitCode, output) = await ProcessRunner.RunAsync("dotnet", args, ctx.SourceDirectory, progress, ct);
|
||||||
|
|
||||||
|
return exitCode == 0
|
||||||
|
? StepResult.Ok()
|
||||||
|
: StepResult.Fail($"dotnet publish failed with exit code {exitCode}");
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/ClaudeDo.Installer/Steps/PublishWorkerStep.cs
Normal file
20
src/ClaudeDo.Installer/Steps/PublishWorkerStep.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using ClaudeDo.Installer.Core;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Installer.Steps;
|
||||||
|
|
||||||
|
public sealed class PublishWorkerStep : IInstallStep
|
||||||
|
{
|
||||||
|
public string Name => "Publish ClaudeDo.Worker";
|
||||||
|
|
||||||
|
public async Task<StepResult> ExecuteAsync(InstallContext ctx, IProgress<string> progress, CancellationToken ct)
|
||||||
|
{
|
||||||
|
progress.Report("Publishing ClaudeDo.Worker...");
|
||||||
|
|
||||||
|
var args = "publish src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release -r win-x64 --self-contained false";
|
||||||
|
var (exitCode, output) = await ProcessRunner.RunAsync("dotnet", args, ctx.SourceDirectory, progress, ct);
|
||||||
|
|
||||||
|
return exitCode == 0
|
||||||
|
? StepResult.Ok()
|
||||||
|
: StepResult.Fail($"dotnet publish failed with exit code {exitCode}");
|
||||||
|
}
|
||||||
|
}
|
||||||
68
src/ClaudeDo.Installer/Steps/RegisterServiceStep.cs
Normal file
68
src/ClaudeDo.Installer/Steps/RegisterServiceStep.cs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
using System.IO;
|
||||||
|
using ClaudeDo.Installer.Core;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Installer.Steps;
|
||||||
|
|
||||||
|
public sealed class RegisterServiceStep : IInstallStep
|
||||||
|
{
|
||||||
|
private const string ServiceName = "ClaudeDoWorker";
|
||||||
|
|
||||||
|
public string Name => "Register Windows Service";
|
||||||
|
|
||||||
|
public async Task<StepResult> ExecuteAsync(InstallContext ctx, IProgress<string> progress, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var workerExe = Path.Combine(ctx.InstallDirectory, "worker", "ClaudeDo.Worker.exe");
|
||||||
|
if (!File.Exists(workerExe))
|
||||||
|
return StepResult.Fail($"Worker executable not found: {workerExe}");
|
||||||
|
|
||||||
|
// Stop existing service (ignore errors — may not exist)
|
||||||
|
progress.Report("Stopping existing service (if any)...");
|
||||||
|
await RunSc($"stop {ServiceName}", ctx, progress, ct, ignoreErrors: true);
|
||||||
|
|
||||||
|
// Delete existing service (ignore errors)
|
||||||
|
progress.Report("Removing existing service registration (if any)...");
|
||||||
|
await RunSc($"delete {ServiceName}", ctx, progress, ct, ignoreErrors: true);
|
||||||
|
|
||||||
|
// Create service
|
||||||
|
var startType = ctx.AutoStart ? "auto" : "demand";
|
||||||
|
var createArgs = $"create {ServiceName} binPath= \"{workerExe}\" start= {startType}";
|
||||||
|
|
||||||
|
if (ctx.ServiceAccount == "CurrentUser")
|
||||||
|
{
|
||||||
|
var username = Environment.UserName;
|
||||||
|
createArgs += $" obj= \".\\{username}\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.Report("Creating service...");
|
||||||
|
var (exitCode, output) = await RunSc(createArgs, ctx, progress, ct);
|
||||||
|
if (exitCode != 0)
|
||||||
|
return StepResult.Fail($"sc.exe create failed (exit {exitCode}): {output}");
|
||||||
|
|
||||||
|
// Configure restart policy
|
||||||
|
var delay = ctx.RestartDelayMs;
|
||||||
|
var failureArgs = $"failure {ServiceName} reset= 86400 actions= restart/{delay}/restart/{delay}/restart/{delay}";
|
||||||
|
progress.Report("Configuring restart policy...");
|
||||||
|
var (failExit, failOutput) = await RunSc(failureArgs, ctx, progress, ct);
|
||||||
|
if (failExit != 0)
|
||||||
|
progress.Report($"Warning: failed to set restart policy (exit {failExit})");
|
||||||
|
|
||||||
|
// Start service if auto-start
|
||||||
|
if (ctx.AutoStart)
|
||||||
|
{
|
||||||
|
progress.Report("Starting service...");
|
||||||
|
var (startExit, _) = await RunSc($"start {ServiceName}", ctx, progress, ct);
|
||||||
|
if (startExit != 0)
|
||||||
|
progress.Report("Warning: service created but failed to start. You may need to start it manually.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return StepResult.Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<(int ExitCode, string Output)> RunSc(
|
||||||
|
string arguments, InstallContext ctx, IProgress<string> progress,
|
||||||
|
CancellationToken ct, bool ignoreErrors = false)
|
||||||
|
{
|
||||||
|
var result = await ProcessRunner.RunAsync("sc.exe", arguments, null, progress, ct);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/ClaudeDo.Installer/Steps/WriteConfigStep.cs
Normal file
42
src/ClaudeDo.Installer/Steps/WriteConfigStep.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using ClaudeDo.Installer.Core;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Installer.Steps;
|
||||||
|
|
||||||
|
public sealed class WriteConfigStep : IInstallStep
|
||||||
|
{
|
||||||
|
public string Name => "Write Configuration";
|
||||||
|
|
||||||
|
public Task<StepResult> ExecuteAsync(InstallContext ctx, IProgress<string> progress, CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var workerCfg = new InstallerWorkerConfig
|
||||||
|
{
|
||||||
|
DbPath = ctx.DbPath,
|
||||||
|
SandboxRoot = ctx.SandboxRoot,
|
||||||
|
LogRoot = ctx.LogRoot,
|
||||||
|
WorktreeRootStrategy = ctx.WorktreeRootStrategy,
|
||||||
|
CentralWorktreeRoot = ctx.CentralWorktreeRoot,
|
||||||
|
QueueBackstopIntervalMs = ctx.QueueBackstopIntervalMs,
|
||||||
|
SignalRPort = ctx.SignalRPort,
|
||||||
|
ClaudeBin = ctx.ClaudeBin,
|
||||||
|
};
|
||||||
|
workerCfg.Save();
|
||||||
|
progress.Report("Written worker.config.json");
|
||||||
|
|
||||||
|
var uiCfg = new InstallerAppSettings
|
||||||
|
{
|
||||||
|
DbPath = ctx.UiDbPath,
|
||||||
|
SignalRUrl = ctx.SignalRUrl,
|
||||||
|
};
|
||||||
|
uiCfg.Save();
|
||||||
|
progress.Report("Written ui.config.json");
|
||||||
|
|
||||||
|
return Task.FromResult(StepResult.Ok());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Task.FromResult(StepResult.Fail(ex.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
280
src/ClaudeDo.Installer/Theme/DarkTheme.xaml
Normal file
280
src/ClaudeDo.Installer/Theme/DarkTheme.xaml
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════
|
||||||
|
Color palette — mirrored from ClaudeDo.App App.axaml
|
||||||
|
═══════════════════════════════════════════════════════════ -->
|
||||||
|
|
||||||
|
<!-- Accent: Forest Teal -->
|
||||||
|
<Color x:Key="AccentColor">#3d9474</Color>
|
||||||
|
<Color x:Key="AccentLightColor">#6bb89e</Color>
|
||||||
|
<SolidColorBrush x:Key="AccentBrush" Color="#3d9474"/>
|
||||||
|
<SolidColorBrush x:Key="AccentLightBrush" Color="#6bb89e"/>
|
||||||
|
<SolidColorBrush x:Key="AccentSubtleBrush" Color="#1A3D9474"/>
|
||||||
|
<SolidColorBrush x:Key="AccentSelectedBrush" Color="#263D9474"/>
|
||||||
|
|
||||||
|
<!-- Text -->
|
||||||
|
<SolidColorBrush x:Key="TextPrimaryBrush" Color="#f1f5f9"/>
|
||||||
|
<SolidColorBrush x:Key="TextSecondaryBrush" Color="#c8d0dc"/>
|
||||||
|
<SolidColorBrush x:Key="TextMutedBrush" Color="#8892a2"/>
|
||||||
|
<SolidColorBrush x:Key="TextDimBrush" Color="#6b7688"/>
|
||||||
|
|
||||||
|
<!-- Borders & Backgrounds -->
|
||||||
|
<SolidColorBrush x:Key="BorderSubtleBrush" Color="#3a3f46"/>
|
||||||
|
<SolidColorBrush x:Key="WindowBgBrush" Color="#1c1e21"/>
|
||||||
|
<SolidColorBrush x:Key="IslandBgBrush" Color="#272a2e"/>
|
||||||
|
<SolidColorBrush x:Key="SidebarBgBrush" Color="#272a2e"/>
|
||||||
|
<SolidColorBrush x:Key="ContentBgBrush" Color="#272a2e"/>
|
||||||
|
|
||||||
|
<!-- Status -->
|
||||||
|
<SolidColorBrush x:Key="StatusGrayBrush" Color="#475569"/>
|
||||||
|
<SolidColorBrush x:Key="StatusOrangeBrush" Color="#e67e22"/>
|
||||||
|
<SolidColorBrush x:Key="StatusGreenBrush" Color="#3d9474"/>
|
||||||
|
<SolidColorBrush x:Key="StatusRedBrush" Color="#ef4444"/>
|
||||||
|
|
||||||
|
<!-- Selection highlights -->
|
||||||
|
<SolidColorBrush x:Key="SelectionBrush" Color="#333d9474"/>
|
||||||
|
<SolidColorBrush x:Key="SelectionHoverBrush" Color="#1A3D9474"/>
|
||||||
|
<SolidColorBrush x:Key="SelectionActiveHoverBrush" Color="#403D9474"/>
|
||||||
|
|
||||||
|
<!-- Validation -->
|
||||||
|
<SolidColorBrush x:Key="ErrorBrush" Color="#ef4444"/>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════
|
||||||
|
Global control styles
|
||||||
|
═══════════════════════════════════════════════════════════ -->
|
||||||
|
|
||||||
|
<!-- Window -->
|
||||||
|
<Style TargetType="Window">
|
||||||
|
<Setter Property="Background" Value="{StaticResource WindowBgBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<Setter Property="FontFamily" Value="Segoe UI"/>
|
||||||
|
<Setter Property="FontSize" Value="13"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- UserControl — transparent so window background shows through -->
|
||||||
|
<Style TargetType="UserControl">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ContentControl — transparent container -->
|
||||||
|
<Style TargetType="ContentControl">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- TextBlock -->
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Label -->
|
||||||
|
<Style TargetType="Label">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextMutedBrush}"/>
|
||||||
|
<Setter Property="FontSize" Value="12"/>
|
||||||
|
<Setter Property="Padding" Value="0,0,0,2"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- TextBox -->
|
||||||
|
<Style TargetType="TextBox">
|
||||||
|
<Setter Property="Background" Value="{StaticResource IslandBgBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderSubtleBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Padding" Value="8,6"/>
|
||||||
|
<Setter Property="CaretBrush" Value="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<Setter Property="SelectionBrush" Value="{StaticResource AccentSubtleBrush}"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsFocused" Value="True">
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsReadOnly" Value="True">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextMutedBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- PasswordBox -->
|
||||||
|
<Style TargetType="PasswordBox">
|
||||||
|
<Setter Property="Background" Value="{StaticResource IslandBgBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderSubtleBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Padding" Value="8,6"/>
|
||||||
|
<Setter Property="CaretBrush" Value="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsFocused" Value="True">
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Button (default) -->
|
||||||
|
<Style TargetType="Button">
|
||||||
|
<Setter Property="Background" Value="{StaticResource IslandBgBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderSubtleBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Padding" Value="16,6"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="Bd"
|
||||||
|
Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
CornerRadius="4"
|
||||||
|
Padding="{TemplateBinding Padding}">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource AccentBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{StaticResource AccentSubtleBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.4"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Accent Button style -->
|
||||||
|
<Style x:Key="AccentButton" TargetType="Button">
|
||||||
|
<Setter Property="Background" Value="{StaticResource AccentBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="Padding" Value="16,6"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="Bd"
|
||||||
|
Background="{TemplateBinding Background}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
CornerRadius="4"
|
||||||
|
Padding="{TemplateBinding Padding}">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{StaticResource AccentLightBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{StaticResource AccentBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.4"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ComboBox -->
|
||||||
|
<Style TargetType="ComboBox">
|
||||||
|
<Setter Property="Background" Value="{StaticResource IslandBgBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderSubtleBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Padding" Value="8,6"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- CheckBox -->
|
||||||
|
<Style TargetType="CheckBox">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<Setter Property="Padding" Value="4,0,0,0"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- RadioButton -->
|
||||||
|
<Style TargetType="RadioButton">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<Setter Property="Padding" Value="4,0,0,0"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ListBox -->
|
||||||
|
<Style TargetType="ListBox">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ListBoxItem -->
|
||||||
|
<Style TargetType="ListBoxItem">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<Setter Property="Padding" Value="10,8"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ListBoxItem">
|
||||||
|
<Border x:Name="Bd"
|
||||||
|
Background="Transparent"
|
||||||
|
CornerRadius="4"
|
||||||
|
Padding="{TemplateBinding Padding}">
|
||||||
|
<ContentPresenter/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{StaticResource SelectionHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsSelected" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{StaticResource SelectionBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<MultiTrigger>
|
||||||
|
<MultiTrigger.Conditions>
|
||||||
|
<Condition Property="IsSelected" Value="True"/>
|
||||||
|
<Condition Property="IsMouseOver" Value="True"/>
|
||||||
|
</MultiTrigger.Conditions>
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{StaticResource SelectionActiveHoverBrush}"/>
|
||||||
|
</MultiTrigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ProgressBar -->
|
||||||
|
<Style TargetType="ProgressBar">
|
||||||
|
<Setter Property="Background" Value="{StaticResource IslandBgBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource AccentBrush}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderSubtleBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Height" Value="6"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ScrollViewer -->
|
||||||
|
<Style TargetType="ScrollViewer">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Border — default transparent -->
|
||||||
|
<Style TargetType="Border">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ItemsControl -->
|
||||||
|
<Style TargetType="ItemsControl">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Separator -->
|
||||||
|
<Style TargetType="Separator">
|
||||||
|
<Setter Property="Background" Value="{StaticResource BorderSubtleBrush}"/>
|
||||||
|
<Setter Property="Height" Value="1"/>
|
||||||
|
<Setter Property="Margin" Value="0,8"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
</ResourceDictionary>
|
||||||
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/ClaudeDo.Installer/app.debug.manifest
Normal file
16
src/ClaudeDo.Installer/app.debug.manifest
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<assemblyIdentity version="1.0.0.0" name="ClaudeDo.Installer"/>
|
||||||
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<security>
|
||||||
|
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||||
|
</requestedPrivileges>
|
||||||
|
</security>
|
||||||
|
</trustInfo>
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
</assembly>
|
||||||
16
src/ClaudeDo.Installer/app.manifest
Normal file
16
src/ClaudeDo.Installer/app.manifest
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<assemblyIdentity version="1.0.0.0" name="ClaudeDo.Installer"/>
|
||||||
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<security>
|
||||||
|
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||||
|
</requestedPrivileges>
|
||||||
|
</security>
|
||||||
|
</trustInfo>
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
</assembly>
|
||||||
Reference in New Issue
Block a user