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:
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