diff --git a/.gitignore b/.gitignore
index 2212214..6fc17d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@
**/obj/
vm/
+.vs/
\ No newline at end of file
diff --git a/authoring-ui/.vs/Covenant.Setup.Authoring/CopilotIndices/18.5.39187.10602/CodeChunks.db b/authoring-ui/.vs/Covenant.Setup.Authoring/CopilotIndices/18.5.39187.10602/CodeChunks.db
index d4775b2..c3d0835 100644
Binary files a/authoring-ui/.vs/Covenant.Setup.Authoring/CopilotIndices/18.5.39187.10602/CodeChunks.db and b/authoring-ui/.vs/Covenant.Setup.Authoring/CopilotIndices/18.5.39187.10602/CodeChunks.db differ
diff --git a/authoring-ui/.vs/Covenant.Setup.Authoring/CopilotIndices/18.5.39187.10602/SemanticSymbols.db b/authoring-ui/.vs/Covenant.Setup.Authoring/CopilotIndices/18.5.39187.10602/SemanticSymbols.db
index e365d26..43b0422 100644
Binary files a/authoring-ui/.vs/Covenant.Setup.Authoring/CopilotIndices/18.5.39187.10602/SemanticSymbols.db and b/authoring-ui/.vs/Covenant.Setup.Authoring/CopilotIndices/18.5.39187.10602/SemanticSymbols.db differ
diff --git a/authoring-ui/.vs/Covenant.Setup.Authoring/DesignTimeBuild/.dtbcache.v2 b/authoring-ui/.vs/Covenant.Setup.Authoring/DesignTimeBuild/.dtbcache.v2
index 86251f7..56dd013 100644
Binary files a/authoring-ui/.vs/Covenant.Setup.Authoring/DesignTimeBuild/.dtbcache.v2 and b/authoring-ui/.vs/Covenant.Setup.Authoring/DesignTimeBuild/.dtbcache.v2 differ
diff --git a/authoring-ui/.vs/Covenant.Setup.Authoring/FileContentIndex/0cd77c29-718e-411f-850b-53b44758d61d.vsidx b/authoring-ui/.vs/Covenant.Setup.Authoring/FileContentIndex/0cd77c29-718e-411f-850b-53b44758d61d.vsidx
deleted file mode 100644
index c6b38d6..0000000
Binary files a/authoring-ui/.vs/Covenant.Setup.Authoring/FileContentIndex/0cd77c29-718e-411f-850b-53b44758d61d.vsidx and /dev/null differ
diff --git a/authoring-ui/.vs/Covenant.Setup.Authoring/FileContentIndex/4122c16d-3df2-4cbd-84fd-6ecc53533e98.vsidx b/authoring-ui/.vs/Covenant.Setup.Authoring/FileContentIndex/4122c16d-3df2-4cbd-84fd-6ecc53533e98.vsidx
deleted file mode 100644
index c403b3f..0000000
Binary files a/authoring-ui/.vs/Covenant.Setup.Authoring/FileContentIndex/4122c16d-3df2-4cbd-84fd-6ecc53533e98.vsidx and /dev/null differ
diff --git a/authoring-ui/.vs/Covenant.Setup.Authoring/FileContentIndex/6f5bea8e-ca23-4e8f-a27b-46e6eb3035e1.vsidx b/authoring-ui/.vs/Covenant.Setup.Authoring/FileContentIndex/6f5bea8e-ca23-4e8f-a27b-46e6eb3035e1.vsidx
deleted file mode 100644
index c403b3f..0000000
Binary files a/authoring-ui/.vs/Covenant.Setup.Authoring/FileContentIndex/6f5bea8e-ca23-4e8f-a27b-46e6eb3035e1.vsidx and /dev/null differ
diff --git a/authoring-ui/.vs/Covenant.Setup.Authoring/FileContentIndex/9fe2b434-4d9e-426e-b9b8-d30c247df3e5.vsidx b/authoring-ui/.vs/Covenant.Setup.Authoring/FileContentIndex/9fe2b434-4d9e-426e-b9b8-d30c247df3e5.vsidx
deleted file mode 100644
index c812455..0000000
Binary files a/authoring-ui/.vs/Covenant.Setup.Authoring/FileContentIndex/9fe2b434-4d9e-426e-b9b8-d30c247df3e5.vsidx and /dev/null differ
diff --git a/authoring-ui/.vs/Covenant.Setup.Authoring/FileContentIndex/f4653db0-d675-449e-8a65-51709e135033.vsidx b/authoring-ui/.vs/Covenant.Setup.Authoring/FileContentIndex/f4653db0-d675-449e-8a65-51709e135033.vsidx
deleted file mode 100644
index 49cbc08..0000000
Binary files a/authoring-ui/.vs/Covenant.Setup.Authoring/FileContentIndex/f4653db0-d675-449e-8a65-51709e135033.vsidx and /dev/null differ
diff --git a/authoring-ui/.vs/Covenant.Setup.Authoring/copilot-chat/1b17085d/sessions/e9acb6e7-3b88-409e-a7a0-33097dcda7ee b/authoring-ui/.vs/Covenant.Setup.Authoring/copilot-chat/1b17085d/sessions/e9acb6e7-3b88-409e-a7a0-33097dcda7ee
index 309e813..74b01c4 100644
Binary files a/authoring-ui/.vs/Covenant.Setup.Authoring/copilot-chat/1b17085d/sessions/e9acb6e7-3b88-409e-a7a0-33097dcda7ee and b/authoring-ui/.vs/Covenant.Setup.Authoring/copilot-chat/1b17085d/sessions/e9acb6e7-3b88-409e-a7a0-33097dcda7ee differ
diff --git a/authoring-ui/.vs/Covenant.Setup.Authoring/v18/.suo b/authoring-ui/.vs/Covenant.Setup.Authoring/v18/.suo
index b598726..1fec327 100644
Binary files a/authoring-ui/.vs/Covenant.Setup.Authoring/v18/.suo and b/authoring-ui/.vs/Covenant.Setup.Authoring/v18/.suo differ
diff --git a/authoring-ui/.vs/Covenant.Setup.Authoring/v18/DocumentLayout.backup.json b/authoring-ui/.vs/Covenant.Setup.Authoring/v18/DocumentLayout.backup.json
index 1323bb9..286ca16 100644
--- a/authoring-ui/.vs/Covenant.Setup.Authoring/v18/DocumentLayout.backup.json
+++ b/authoring-ui/.vs/Covenant.Setup.Authoring/v18/DocumentLayout.backup.json
@@ -2,6 +2,10 @@
"Version": 1,
"WorkspaceRootPath": "C:\\Users\\jasonross\\workspace\\covenant-setup\\authoring-ui\\",
"Documents": [
+ {
+ "AbsoluteMoniker": "D:0:0:{5D26E66D-8F74-39AC-8CD2-24BC7C6E0C0F}|Covenant.Setup.Authoring.csproj|c:\\users\\jasonross\\workspace\\covenant-setup\\authoring-ui\\mainwindow.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
+ "RelativeMoniker": "D:0:0:{5D26E66D-8F74-39AC-8CD2-24BC7C6E0C0F}|Covenant.Setup.Authoring.csproj|solutionrelative:mainwindow.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
+ },
{
"AbsoluteMoniker": "D:0:0:{5D26E66D-8F74-39AC-8CD2-24BC7C6E0C0F}|Covenant.Setup.Authoring.csproj|c:\\users\\jasonross\\workspace\\covenant-setup\\authoring-ui\\app.xaml||{F11ACC28-31D1-4C80-A34B-F4E09D3D753C}",
"RelativeMoniker": "D:0:0:{5D26E66D-8F74-39AC-8CD2-24BC7C6E0C0F}|Covenant.Setup.Authoring.csproj|solutionrelative:app.xaml||{F11ACC28-31D1-4C80-A34B-F4E09D3D753C}"
@@ -18,11 +22,24 @@
"DocumentGroups": [
{
"DockedWidth": 200,
- "SelectedChildIndex": 1,
+ "SelectedChildIndex": 0,
"Children": [
{
"$type": "Document",
- "DocumentIndex": 1,
+ "DocumentIndex": 0,
+ "Title": "MainWindow.cs",
+ "DocumentMoniker": "C:\\Users\\jasonross\\workspace\\covenant-setup\\authoring-ui\\MainWindow.cs",
+ "RelativeDocumentMoniker": "MainWindow.cs",
+ "ToolTip": "C:\\Users\\jasonross\\workspace\\covenant-setup\\authoring-ui\\MainWindow.cs",
+ "RelativeToolTip": "MainWindow.cs",
+ "ViewState": "AgIAABsBAAAAAAAAAIA5wBsBAAAAAAAAAAAAAA==",
+ "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
+ "WhenOpened": "2026-05-05T23:22:18.637Z",
+ "EditorCaption": ""
+ },
+ {
+ "$type": "Document",
+ "DocumentIndex": 2,
"Title": "Program.cs",
"DocumentMoniker": "C:\\Users\\jasonross\\workspace\\covenant-setup\\authoring-ui\\Program.cs",
"RelativeDocumentMoniker": "Program.cs",
@@ -30,12 +47,11 @@
"RelativeToolTip": "Program.cs",
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAsAAAAAAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
- "WhenOpened": "2026-05-05T00:32:51.733Z",
- "EditorCaption": ""
+ "WhenOpened": "2026-05-05T00:32:51.733Z"
},
{
"$type": "Document",
- "DocumentIndex": 0,
+ "DocumentIndex": 1,
"Title": "App.xaml",
"DocumentMoniker": "C:\\Users\\jasonross\\workspace\\covenant-setup\\authoring-ui\\App.xaml",
"RelativeDocumentMoniker": "App.xaml",
diff --git a/authoring-ui/.vs/Covenant.Setup.Authoring/v18/DocumentLayout.json b/authoring-ui/.vs/Covenant.Setup.Authoring/v18/DocumentLayout.json
index 1323bb9..9b180b3 100644
--- a/authoring-ui/.vs/Covenant.Setup.Authoring/v18/DocumentLayout.json
+++ b/authoring-ui/.vs/Covenant.Setup.Authoring/v18/DocumentLayout.json
@@ -2,6 +2,14 @@
"Version": 1,
"WorkspaceRootPath": "C:\\Users\\jasonross\\workspace\\covenant-setup\\authoring-ui\\",
"Documents": [
+ {
+ "AbsoluteMoniker": "D:0:0:{5D26E66D-8F74-39AC-8CD2-24BC7C6E0C0F}|Covenant.Setup.Authoring.csproj|c:\\users\\jasonross\\workspace\\covenant-setup\\authoring-ui\\installerconfigdialog.xaml||{F11ACC28-31D1-4C80-A34B-F4E09D3D753C}",
+ "RelativeMoniker": "D:0:0:{5D26E66D-8F74-39AC-8CD2-24BC7C6E0C0F}|Covenant.Setup.Authoring.csproj|solutionrelative:installerconfigdialog.xaml||{F11ACC28-31D1-4C80-A34B-F4E09D3D753C}"
+ },
+ {
+ "AbsoluteMoniker": "D:0:0:{5D26E66D-8F74-39AC-8CD2-24BC7C6E0C0F}|Covenant.Setup.Authoring.csproj|c:\\users\\jasonross\\workspace\\covenant-setup\\authoring-ui\\mainwindow.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
+ "RelativeMoniker": "D:0:0:{5D26E66D-8F74-39AC-8CD2-24BC7C6E0C0F}|Covenant.Setup.Authoring.csproj|solutionrelative:mainwindow.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
+ },
{
"AbsoluteMoniker": "D:0:0:{5D26E66D-8F74-39AC-8CD2-24BC7C6E0C0F}|Covenant.Setup.Authoring.csproj|c:\\users\\jasonross\\workspace\\covenant-setup\\authoring-ui\\app.xaml||{F11ACC28-31D1-4C80-A34B-F4E09D3D753C}",
"RelativeMoniker": "D:0:0:{5D26E66D-8F74-39AC-8CD2-24BC7C6E0C0F}|Covenant.Setup.Authoring.csproj|solutionrelative:app.xaml||{F11ACC28-31D1-4C80-A34B-F4E09D3D753C}"
@@ -18,11 +26,37 @@
"DocumentGroups": [
{
"DockedWidth": 200,
- "SelectedChildIndex": 1,
+ "SelectedChildIndex": 0,
"Children": [
+ {
+ "$type": "Document",
+ "DocumentIndex": 0,
+ "Title": "InstallerConfigDialog.xaml",
+ "DocumentMoniker": "C:\\Users\\jasonross\\workspace\\covenant-setup\\authoring-ui\\InstallerConfigDialog.xaml",
+ "RelativeDocumentMoniker": "InstallerConfigDialog.xaml",
+ "ToolTip": "C:\\Users\\jasonross\\workspace\\covenant-setup\\authoring-ui\\InstallerConfigDialog.xaml*",
+ "RelativeToolTip": "InstallerConfigDialog.xaml*",
+ "ViewState": "AgIAABwAAAAAAAAAAAAAACMAAAAZAAAAAAAAAA==",
+ "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003549|",
+ "WhenOpened": "2026-05-06T00:18:02.89Z",
+ "EditorCaption": ""
+ },
{
"$type": "Document",
"DocumentIndex": 1,
+ "Title": "MainWindow.cs",
+ "DocumentMoniker": "C:\\Users\\jasonross\\workspace\\covenant-setup\\authoring-ui\\MainWindow.cs",
+ "RelativeDocumentMoniker": "MainWindow.cs",
+ "ToolTip": "C:\\Users\\jasonross\\workspace\\covenant-setup\\authoring-ui\\MainWindow.cs",
+ "RelativeToolTip": "MainWindow.cs",
+ "ViewState": "AgIAABsBAAAAAAAAAIA5wBsBAAAAAAAAAAAAAA==",
+ "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
+ "WhenOpened": "2026-05-05T23:22:18.637Z",
+ "EditorCaption": ""
+ },
+ {
+ "$type": "Document",
+ "DocumentIndex": 3,
"Title": "Program.cs",
"DocumentMoniker": "C:\\Users\\jasonross\\workspace\\covenant-setup\\authoring-ui\\Program.cs",
"RelativeDocumentMoniker": "Program.cs",
@@ -30,12 +64,11 @@
"RelativeToolTip": "Program.cs",
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAsAAAAAAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
- "WhenOpened": "2026-05-05T00:32:51.733Z",
- "EditorCaption": ""
+ "WhenOpened": "2026-05-05T00:32:51.733Z"
},
{
"$type": "Document",
- "DocumentIndex": 0,
+ "DocumentIndex": 2,
"Title": "App.xaml",
"DocumentMoniker": "C:\\Users\\jasonross\\workspace\\covenant-setup\\authoring-ui\\App.xaml",
"RelativeDocumentMoniker": "App.xaml",
diff --git a/authoring-ui/InstallerConfigDialog.xaml b/authoring-ui/InstallerConfigDialog.xaml
new file mode 100644
index 0000000..7f0feb8
--- /dev/null
+++ b/authoring-ui/InstallerConfigDialog.xaml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/authoring-ui/InstallerConfigDialog.xaml.cs b/authoring-ui/InstallerConfigDialog.xaml.cs
new file mode 100644
index 0000000..7878200
--- /dev/null
+++ b/authoring-ui/InstallerConfigDialog.xaml.cs
@@ -0,0 +1,64 @@
+using Microsoft.UI.Xaml.Controls;
+using Windows.Storage.Pickers;
+using WinRT.Interop;
+
+namespace Covenant.Setup.Authoring;
+
+internal sealed partial class InstallerConfigDialog : ContentDialog
+{
+ private readonly InstallerConfigViewModel _viewModel;
+ private readonly IntPtr _ownerWindowHandle;
+
+ public InstallerConfigDialog(InstallerConfigViewModel viewModel, IntPtr ownerWindowHandle)
+ {
+ _viewModel = viewModel;
+ _ownerWindowHandle = ownerWindowHandle;
+ InitializeComponent();
+ DataContext = viewModel;
+ }
+
+ private async void BrowseCovenantSetupButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs args)
+ {
+ var picker = new FileOpenPicker
+ {
+ SuggestedStartLocation = PickerLocationId.ComputerFolder
+ };
+ InitializeWithWindow.Initialize(picker, _ownerWindowHandle);
+ picker.FileTypeFilter.Add(".exe");
+
+ var file = await picker.PickSingleFileAsync();
+ if (file is not null)
+ {
+ _ = await _viewModel.AcceptToolPathAsync(file.Path);
+ }
+ }
+
+ private async void ChooseOutputDirectoryButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs args)
+ {
+ var picker = new FolderPicker
+ {
+ SuggestedStartLocation = PickerLocationId.DocumentsLibrary
+ };
+ InitializeWithWindow.Initialize(picker, _ownerWindowHandle);
+ picker.FileTypeFilter.Add("*");
+
+ var folder = await picker.PickSingleFolderAsync();
+ if (folder is not null)
+ {
+ _viewModel.OutputDirectory = folder.Path;
+ }
+ }
+
+ private async void SaveButton_Click(ContentDialog sender, ContentDialogButtonClickEventArgs args)
+ {
+ var deferral = args.GetDeferral();
+ try
+ {
+ args.Cancel = !await _viewModel.SaveConfigAsync();
+ }
+ finally
+ {
+ deferral.Complete();
+ }
+ }
+}
diff --git a/authoring-ui/InstallerConfigViewModel.cs b/authoring-ui/InstallerConfigViewModel.cs
new file mode 100644
index 0000000..a71b768
--- /dev/null
+++ b/authoring-ui/InstallerConfigViewModel.cs
@@ -0,0 +1,434 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Windows.Input;
+
+namespace Covenant.Setup.Authoring;
+
+internal enum InstallerConfigStatusKind
+{
+ Neutral,
+ Success,
+ Error
+}
+
+internal sealed class InstallerConfigViewModel : INotifyPropertyChanged
+{
+ private const string MissingToolStatus = "covenant-setup.exe was not found. Packaging is disabled.";
+ private const string UnvalidatedToolStatus = "covenant-setup.exe was not validated. Packaging is disabled.";
+
+ private readonly Func _locateCovenantSetupTool;
+ private readonly Func> _validateCovenantSetupTool;
+ private CovenantSetupTool? _covenantSetupTool;
+ private string _covenantSetupPath = string.Empty;
+ private string _outputDirectory = Path.Combine(Environment.CurrentDirectory, "dist");
+ private string _statusText = MissingToolStatus;
+ private string _messageText = string.Empty;
+ private InstallerConfigStatusKind _statusKind = InstallerConfigStatusKind.Error;
+ private InstallerConfigStatusKind _messageKind = InstallerConfigStatusKind.Neutral;
+ private bool _isCheckingTool;
+ private bool _isBuilding;
+ private bool _hasManifestValidationErrors;
+ private bool _syncingAcceptedToolPath;
+
+ public InstallerConfigViewModel()
+ : this(CovenantSetupToolLocator.Find, CovenantSetupToolValidator.ValidateAsync)
+ {
+ }
+
+ internal InstallerConfigViewModel(
+ Func locateCovenantSetupTool,
+ Func> validateCovenantSetupTool)
+ {
+ _locateCovenantSetupTool = locateCovenantSetupTool;
+ _validateCovenantSetupTool = validateCovenantSetupTool;
+ RefreshToolCheckCommand = new RelayCommand(RefreshToolCheck, () => !IsBusy);
+ ValidateToolPathCommand = new AsyncRelayCommand(ValidateToolPathAsync, () => CanValidateToolPath);
+ SaveConfigCommand = new AsyncRelayCommand(SaveConfigAsync, () => CanSaveConfig);
+ RefreshToolCheck();
+ }
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ public ICommand RefreshToolCheckCommand { get; }
+
+ public ICommand ValidateToolPathCommand { get; }
+
+ public ICommand SaveConfigCommand { get; }
+
+ public CovenantSetupTool? CovenantSetupTool
+ {
+ get => _covenantSetupTool;
+ private set
+ {
+ if (SetProperty(ref _covenantSetupTool, value))
+ {
+ OnPropertyChanged(nameof(HasCovenantSetupTool));
+ OnPropertyChanged(nameof(CanSaveConfig));
+ RaiseCommandStatesChanged();
+ }
+ }
+ }
+
+ public bool HasCovenantSetupTool => CovenantSetupTool is not null;
+
+ public string CovenantSetupPath
+ {
+ get => _covenantSetupPath;
+ set
+ {
+ if (!SetProperty(ref _covenantSetupPath, value))
+ {
+ return;
+ }
+
+ OnPropertyChanged(nameof(CanValidateToolPath));
+ RaiseCommandStatesChanged();
+
+ if (!_syncingAcceptedToolPath && !IsAcceptedToolPath(value))
+ {
+ ClearValidatedTool(UnvalidatedToolStatus);
+ }
+ }
+ }
+
+ public string OutputDirectory
+ {
+ get => _outputDirectory;
+ set
+ {
+ if (SetProperty(ref _outputDirectory, value))
+ {
+ OnPropertyChanged(nameof(CanSaveConfig));
+ RaiseCommandStatesChanged();
+ }
+ }
+ }
+
+ public string StatusText
+ {
+ get => _statusText;
+ private set => SetProperty(ref _statusText, value);
+ }
+
+ public InstallerConfigStatusKind StatusKind
+ {
+ get => _statusKind;
+ private set => SetProperty(ref _statusKind, value);
+ }
+
+ public string MessageText
+ {
+ get => _messageText;
+ private set => SetProperty(ref _messageText, value);
+ }
+
+ public InstallerConfigStatusKind MessageKind
+ {
+ get => _messageKind;
+ private set => SetProperty(ref _messageKind, value);
+ }
+
+ public bool IsCheckingTool
+ {
+ get => _isCheckingTool;
+ private set
+ {
+ if (SetProperty(ref _isCheckingTool, value))
+ {
+ NotifyBusyStateChanged();
+ }
+ }
+ }
+
+ public bool IsBuilding
+ {
+ get => _isBuilding;
+ set
+ {
+ if (SetProperty(ref _isBuilding, value))
+ {
+ NotifyBusyStateChanged();
+ }
+ }
+ }
+
+ public bool IsBusy => IsCheckingTool || IsBuilding;
+
+ public bool HasManifestValidationErrors
+ {
+ get => _hasManifestValidationErrors;
+ private set
+ {
+ if (SetProperty(ref _hasManifestValidationErrors, value))
+ {
+ OnPropertyChanged(nameof(CanSaveConfig));
+ RaiseCommandStatesChanged();
+ }
+ }
+ }
+
+ public bool CanValidateToolPath =>
+ !IsBusy &&
+ !string.IsNullOrWhiteSpace(CovenantSetupPath);
+
+ public bool CanSaveConfig =>
+ !IsBusy &&
+ !HasManifestValidationErrors &&
+ !string.IsNullOrWhiteSpace(CovenantSetupPath) &&
+ !string.IsNullOrWhiteSpace(OutputDirectory);
+
+ public void RefreshToolCheck()
+ {
+ if (IsBusy)
+ {
+ return;
+ }
+
+ var tool = _locateCovenantSetupTool();
+ if (tool is null)
+ {
+ ClearValidatedTool(MissingToolStatus);
+ SetMessage(string.Empty, InstallerConfigStatusKind.Neutral);
+ return;
+ }
+
+ AcceptValidatedTool(tool);
+ SetMessage(string.Empty, InstallerConfigStatusKind.Neutral);
+ }
+
+ public async Task ValidateToolPathAsync(CancellationToken cancellationToken = default)
+ {
+ return await AcceptToolPathAsync(CovenantSetupPath, cancellationToken);
+ }
+
+ public async Task AcceptToolPathAsync(
+ string path,
+ CancellationToken cancellationToken = default)
+ {
+ if (IsBusy)
+ {
+ return false;
+ }
+
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ SyncToolPath(string.Empty);
+ ClearValidatedTool(MissingToolStatus);
+ SetMessage(string.Empty, InstallerConfigStatusKind.Neutral);
+ return false;
+ }
+
+ IsCheckingTool = true;
+ SetMessage("Checking covenant-setup...", InstallerConfigStatusKind.Neutral);
+ try
+ {
+ SyncToolPath(path.Trim());
+ var result = await _validateCovenantSetupTool(CovenantSetupPath, cancellationToken);
+ if (result.IsValid && result.Tool is not null)
+ {
+ AcceptValidatedTool(result.Tool);
+ SetMessage(string.Empty, InstallerConfigStatusKind.Neutral);
+ return true;
+ }
+
+ ClearValidatedTool(result.Message);
+ SetMessage(
+ result.Message + " Pick a valid covenant-setup executable.",
+ InstallerConfigStatusKind.Error);
+ return false;
+ }
+ finally
+ {
+ IsCheckingTool = false;
+ }
+ }
+
+ public void SetManifestValidationState(bool hasErrors)
+ {
+ HasManifestValidationErrors = hasErrors;
+ }
+
+ public async Task SaveConfigAsync(CancellationToken cancellationToken = default)
+ {
+ if (IsBuilding)
+ {
+ SetMessage("Installer configuration cannot be saved while a build is running.", InstallerConfigStatusKind.Error);
+ return false;
+ }
+
+ if (HasManifestValidationErrors)
+ {
+ SetMessage("Resolve manifest validation errors before saving installer configuration.", InstallerConfigStatusKind.Error);
+ return false;
+ }
+
+ if (!IsAcceptedToolPath(CovenantSetupPath))
+ {
+ var accepted = await ValidateToolPathAsync(cancellationToken);
+ if (!accepted)
+ {
+ return false;
+ }
+ }
+
+ if (string.IsNullOrWhiteSpace(OutputDirectory))
+ {
+ SetMessage("Choose an output directory before saving installer configuration.", InstallerConfigStatusKind.Error);
+ return false;
+ }
+
+ SetMessage("Installer configuration saved.", InstallerConfigStatusKind.Success);
+ return true;
+ }
+
+ public void ClearMessage()
+ {
+ SetMessage(string.Empty, InstallerConfigStatusKind.Neutral);
+ }
+
+ private void AcceptValidatedTool(CovenantSetupTool tool)
+ {
+ CovenantSetupTool = tool;
+ SyncToolPath(tool.Path);
+ StatusText = "Packaging enabled: " + tool.Path;
+ StatusKind = InstallerConfigStatusKind.Success;
+ }
+
+ private void ClearValidatedTool(string statusText)
+ {
+ CovenantSetupTool = null;
+ StatusText = statusText;
+ StatusKind = InstallerConfigStatusKind.Error;
+ }
+
+ private void SyncToolPath(string path)
+ {
+ _syncingAcceptedToolPath = true;
+ try
+ {
+ CovenantSetupPath = path;
+ }
+ finally
+ {
+ _syncingAcceptedToolPath = false;
+ }
+ }
+
+ private bool IsAcceptedToolPath(string path) =>
+ CovenantSetupTool is not null &&
+ string.Equals(path?.Trim(), CovenantSetupTool.Path, StringComparison.OrdinalIgnoreCase);
+
+ private void SetMessage(string message, InstallerConfigStatusKind kind)
+ {
+ MessageText = message;
+ MessageKind = kind;
+ }
+
+ private void NotifyBusyStateChanged()
+ {
+ OnPropertyChanged(nameof(IsBusy));
+ OnPropertyChanged(nameof(CanValidateToolPath));
+ OnPropertyChanged(nameof(CanSaveConfig));
+ RaiseCommandStatesChanged();
+ }
+
+ private bool SetProperty(
+ ref T field,
+ T value,
+ [CallerMemberName] string? propertyName = null)
+ {
+ if (EqualityComparer.Default.Equals(field, value))
+ {
+ return false;
+ }
+
+ field = value;
+ OnPropertyChanged(propertyName);
+ return true;
+ }
+
+ private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+ {
+ if (propertyName is not null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+
+ private void RaiseCommandStatesChanged()
+ {
+ ((RelayCommand)RefreshToolCheckCommand).RaiseCanExecuteChanged();
+ ((AsyncRelayCommand)ValidateToolPathCommand).RaiseCanExecuteChanged();
+ ((AsyncRelayCommand)SaveConfigCommand).RaiseCanExecuteChanged();
+ }
+
+ private sealed class RelayCommand : ICommand
+ {
+ private readonly Action _execute;
+ private readonly Func? _canExecute;
+
+ public RelayCommand(Action execute, Func? canExecute = null)
+ {
+ _execute = execute;
+ _canExecute = canExecute;
+ }
+
+ public event EventHandler? CanExecuteChanged;
+
+ public bool CanExecute(object? parameter) => _canExecute?.Invoke() ?? true;
+
+ public void Execute(object? parameter) => _execute();
+
+ public void RaiseCanExecuteChanged()
+ {
+ CanExecuteChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+
+ private sealed class AsyncRelayCommand : ICommand
+ {
+ private readonly Func _execute;
+ private readonly Func? _canExecute;
+ private CancellationTokenSource? _currentExecution;
+
+ public AsyncRelayCommand(
+ Func execute,
+ Func? canExecute = null)
+ {
+ _execute = execute;
+ _canExecute = canExecute;
+ }
+
+ public event EventHandler? CanExecuteChanged;
+
+ public bool CanExecute(object? parameter) =>
+ _currentExecution is null &&
+ (_canExecute?.Invoke() ?? true);
+
+ public async void Execute(object? parameter)
+ {
+ if (!CanExecute(parameter))
+ {
+ return;
+ }
+
+ _currentExecution = new CancellationTokenSource();
+ RaiseCanExecuteChanged();
+ try
+ {
+ await _execute(_currentExecution.Token);
+ }
+ finally
+ {
+ _currentExecution?.Dispose();
+ _currentExecution = null;
+ RaiseCanExecuteChanged();
+ }
+ }
+
+ public void RaiseCanExecuteChanged()
+ {
+ CanExecuteChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+}
diff --git a/authoring-ui/MainWindow.cs b/authoring-ui/MainWindow.cs
index 15db603..6928bcd 100644
--- a/authoring-ui/MainWindow.cs
+++ b/authoring-ui/MainWindow.cs
@@ -20,19 +20,13 @@ namespace Covenant.Setup.Authoring;
internal sealed class MainWindow : Window
{
private readonly MainViewModel _viewModel = new();
+ private readonly InstallerConfigViewModel _installerConfigViewModel = new();
private readonly List _sectionBorders = [];
private readonly List _rowBorders = [];
private readonly List _secondaryTextBlocks = [];
private TextBlock _statusText = null!;
- private TextBlock _toolStatusText = null!;
- private TextBlock _installerDialogMessageText = null!;
private TextBox _previewBox = null!;
private Button _copyPreviewButton = null!;
- private TextBox _covenantSetupPathBox = null!;
- private Button _browseCovenantSetupButton = null!;
- private Button _refreshToolButton = null!;
- private TextBox _outputDirectoryBox = null!;
- private Button _chooseOutputButton = null!;
private Button _generateInstallerButton = null!;
private Grid _rootGrid = null!;
private ToggleSwitch _themeToggle = null!;
@@ -41,7 +35,6 @@ internal sealed class MainWindow : Window
private int _copyPreviewFeedbackVersion;
private bool _syncingThemeToggle;
private bool _usingSystemTheme;
- private bool _syncingCovenantSetupPath;
public MainWindow()
{
@@ -56,10 +49,7 @@ internal sealed class MainWindow : Window
RefreshStatusBrush();
}
if (args.PropertyName is nameof(MainViewModel.HasCovenantSetupTool)
- or nameof(MainViewModel.CanPackage)
- or nameof(MainViewModel.CovenantSetupStatus)
- or nameof(MainViewModel.CovenantSetupPath)
- or nameof(MainViewModel.OutputDirectory))
+ or nameof(MainViewModel.CanPackage))
{
RefreshPackageControls();
}
@@ -291,97 +281,6 @@ internal sealed class MainWindow : Window
Labeled("Primary Payload", payloadBox));
}
- private FrameworkElement BuildInstallerConfigContent()
- {
- _toolStatusText = new TextBlock
- {
- TextWrapping = TextWrapping.Wrap
- };
- _toolStatusText.SetBinding(
- TextBlock.TextProperty,
- new Binding
- {
- Path = new PropertyPath(nameof(MainViewModel.CovenantSetupStatus)),
- Mode = BindingMode.OneWay
- });
-
- _covenantSetupPathBox = new TextBox
- {
- PlaceholderText = @"C:\path\to\covenant-setup.exe",
- Text = _viewModel.CovenantSetupPath
- };
- _covenantSetupPathBox.TextChanged += (_, _) =>
- {
- if (_syncingCovenantSetupPath)
- {
- return;
- }
-
- if (!string.Equals(_covenantSetupPathBox.Text, _viewModel.CovenantSetupPath, StringComparison.OrdinalIgnoreCase))
- {
- _viewModel.RejectCovenantSetupTool("covenant-setup.exe was not validated. Packaging is disabled.");
- }
- };
- _covenantSetupPathBox.LostFocus += async (_, _) => await ValidateTypedCovenantSetupPathAsync(showInvalidMessage: true);
-
- _browseCovenantSetupButton = new Button
- {
- Content = "Browse",
- Width = 96
- };
- _browseCovenantSetupButton.Click += async (_, _) => await BrowseCovenantSetupPathAsync();
-
- _outputDirectoryBox = BoundTextBox(nameof(MainViewModel.OutputDirectory), @"C:\path\to\dist");
-
- _chooseOutputButton = new Button
- {
- Content = "Choose",
- Width = 96
- };
- _chooseOutputButton.Click += async (_, _) => await ChooseOutputDirectoryAsync();
-
- _refreshToolButton = new Button
- {
- Content = "Refresh Tool Check",
- Width = 156
- };
- _refreshToolButton.Click += (_, _) =>
- {
- _viewModel.RefreshCovenantSetupTool();
- SyncCovenantSetupPathBox();
- SetInstallerDialogMessage(string.Empty);
- };
-
- var actionRow = new StackPanel
- {
- Orientation = Orientation.Horizontal,
- HorizontalAlignment = HorizontalAlignment.Right,
- Spacing = 8
- };
- actionRow.Children.Add(_refreshToolButton);
-
- _installerDialogMessageText = new TextBlock
- {
- TextWrapping = TextWrapping.Wrap,
- MaxLines = 6
- };
- _secondaryTextBlocks.Add(_installerDialogMessageText);
-
- var stack = new StackPanel
- {
- Spacing = 10,
- MinWidth = 640,
- HorizontalAlignment = HorizontalAlignment.Stretch
- };
- stack.Children.Add(_toolStatusText);
- stack.Children.Add(DialogFieldGrid(
- ("Covenant Setup Executable", _covenantSetupPathBox, _browseCovenantSetupButton),
- ("Output Directory", _outputDirectoryBox, _chooseOutputButton)));
- stack.Children.Add(actionRow);
- stack.Children.Add(_installerDialogMessageText);
- return stack;
- }
-
private FrameworkElement BuildDirectoriesSection()
{
var pathBox = new TextBox { PlaceholderText = @"{LocalAppData}\Vendor\App" };
@@ -505,42 +404,24 @@ internal sealed class MainWindow : Window
private async Task ShowInstallerConfigAsync()
{
- var dialog = new ContentDialog
- {
- Title = "Installer Config",
- CloseButtonText = "Close",
- Width = 1080,
- MinWidth = 1080,
- Content = BuildInstallerConfigContent()
- };
+ _installerConfigViewModel.SetManifestValidationState(_viewModel.HasValidationErrors);
+ _installerConfigViewModel.IsBuilding = _isPackaging;
+ var dialog = new InstallerConfigDialog(
+ _installerConfigViewModel,
+ WindowNative.GetWindowHandle(this));
if (Content is FrameworkElement root)
{
dialog.XamlRoot = root.XamlRoot;
}
- dialog.Closing += (_, args) =>
+ var result = await dialog.ShowAsync();
+ if (result == ContentDialogResult.Primary)
{
- if (_isPackaging)
- {
- args.Cancel = true;
- }
- };
-
- RefreshPackageControls();
- _ = await dialog.ShowAsync();
-
- if (_installerDialogMessageText is not null)
- {
- _secondaryTextBlocks.Remove(_installerDialogMessageText);
+ _viewModel.SetCovenantSetupTool(_installerConfigViewModel.CovenantSetupTool);
+ _viewModel.OutputDirectory = _installerConfigViewModel.OutputDirectory;
+ RefreshPackageControls();
}
- _toolStatusText = null!;
- _installerDialogMessageText = null!;
- _covenantSetupPathBox = null!;
- _browseCovenantSetupButton = null!;
- _refreshToolButton = null!;
- _outputDirectoryBox = null!;
- _chooseOutputButton = null!;
}
private async Task SaveManifestAsync()
@@ -614,95 +495,6 @@ internal sealed class MainWindow : Window
return file.Path;
}
- private async Task BrowseCovenantSetupPathAsync()
- {
- var picker = new FileOpenPicker();
- InitializeWithWindow.Initialize(picker, WindowNative.GetWindowHandle(this));
- picker.SuggestedStartLocation = PickerLocationId.ComputerFolder;
- picker.FileTypeFilter.Add(".exe");
-
- var file = await picker.PickSingleFileAsync();
- if (file is not null)
- {
- await AcceptCovenantSetupPathAsync(file.Path, showInvalidMessage: true);
- }
- }
-
- private async Task ValidateTypedCovenantSetupPathAsync(bool showInvalidMessage)
- {
- if (_covenantSetupPathBox is null)
- {
- return;
- }
-
- var path = _covenantSetupPathBox.Text.Trim();
- if (string.IsNullOrWhiteSpace(path))
- {
- _viewModel.RejectCovenantSetupTool("covenant-setup.exe was not found. Packaging is disabled.");
- SetInstallerDialogMessage(string.Empty);
- return;
- }
-
- if (string.Equals(path, _viewModel.CovenantSetupPath, StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
-
- await AcceptCovenantSetupPathAsync(path, showInvalidMessage);
- }
-
- private async Task AcceptCovenantSetupPathAsync(string path, bool showInvalidMessage)
- {
- SetInstallerDialogMessage("Checking covenant-setup...");
- var result = await CovenantSetupToolValidator.ValidateAsync(path, CancellationToken.None);
- if (result.IsValid && result.Tool is not null)
- {
- _viewModel.SetCovenantSetupTool(result.Tool);
- SyncCovenantSetupPathBox();
- SetInstallerDialogMessage(string.Empty);
- return true;
- }
-
- _viewModel.RejectCovenantSetupTool(result.Message);
- if (showInvalidMessage)
- {
- SetInstallerDialogMessage(result.Message + " Run the help check by selecting the real covenant-setup.exe.");
- }
- return false;
- }
-
- private void SyncCovenantSetupPathBox()
- {
- if (_covenantSetupPathBox is null)
- {
- return;
- }
-
- _syncingCovenantSetupPath = true;
- try
- {
- _covenantSetupPathBox.Text = _viewModel.CovenantSetupPath;
- }
- finally
- {
- _syncingCovenantSetupPath = false;
- }
- }
-
- private async Task ChooseOutputDirectoryAsync()
- {
- var picker = new FolderPicker();
- InitializeWithWindow.Initialize(picker, WindowNative.GetWindowHandle(this));
- picker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary;
- picker.FileTypeFilter.Add("*");
-
- var folder = await picker.PickSingleFolderAsync();
- if (folder is not null)
- {
- _viewModel.OutputDirectory = folder.Path;
- }
- }
-
private async Task GenerateInstallerAsync(Func? showMessageAsync = null)
{
showMessageAsync ??= ShowNoticeAsync;
@@ -734,6 +526,7 @@ internal sealed class MainWindow : Window
_lastSavedManifestPath = manifestPath;
_isPackaging = true;
+ _installerConfigViewModel.IsBuilding = true;
RefreshPackageControls();
try
{
@@ -758,6 +551,7 @@ internal sealed class MainWindow : Window
finally
{
_isPackaging = false;
+ _installerConfigViewModel.IsBuilding = false;
RefreshPackageControls();
}
}
@@ -806,14 +600,6 @@ internal sealed class MainWindow : Window
AutomationProperties.SetName(_copyPreviewButton, "Copy TOML preview");
}
- private void SetInstallerDialogMessage(string message)
- {
- if (_installerDialogMessageText is not null)
- {
- _installerDialogMessageText.Text = message;
- }
- }
-
private async Task ShowNoticeAsync(string title, string message)
{
var dialog = new ContentDialog
@@ -924,39 +710,11 @@ internal sealed class MainWindow : Window
private void RefreshPackageControls()
{
- var controlsAvailable = !_isPackaging;
- if (_covenantSetupPathBox is not null)
- {
- _covenantSetupPathBox.IsEnabled = controlsAvailable;
- }
- if (_browseCovenantSetupButton is not null)
- {
- _browseCovenantSetupButton.IsEnabled = controlsAvailable;
- }
- if (_refreshToolButton is not null)
- {
- _refreshToolButton.IsEnabled = controlsAvailable;
- }
- if (_outputDirectoryBox is not null)
- {
- _outputDirectoryBox.IsEnabled = controlsAvailable;
- }
- if (_chooseOutputButton is not null)
- {
- _chooseOutputButton.IsEnabled = controlsAvailable;
- }
if (_generateInstallerButton is not null)
{
_generateInstallerButton.IsEnabled = _viewModel.HasCovenantSetupTool && !_isPackaging;
_generateInstallerButton.Content = _isPackaging ? "Building..." : "Save and Build";
}
- var brushes = CreateThemeBrushes();
- if (_toolStatusText is not null)
- {
- _toolStatusText.Foreground = _viewModel.HasCovenantSetupTool
- ? brushes.SuccessText
- : brushes.ErrorText;
- }
}
private void ApplyThemeBrushes()
@@ -994,20 +752,6 @@ internal sealed class MainWindow : Window
}
RefreshStatusBrush();
- RefreshPackageStatusBrush();
- }
-
- private void RefreshPackageStatusBrush()
- {
- if (_toolStatusText is null)
- {
- return;
- }
-
- var brushes = CreateThemeBrushes();
- _toolStatusText.Foreground = _viewModel.HasCovenantSetupTool
- ? brushes.SuccessText
- : brushes.ErrorText;
}
private ElementTheme EffectiveTheme()
@@ -1123,47 +867,6 @@ internal sealed class MainWindow : Window
return stack;
}
- private static Grid DialogFieldGrid(params (string Label, TextBox TextBox, Button Button)[] fields)
- {
- var grid = new Grid
- {
- ColumnSpacing = 6,
- RowSpacing = 2,
- HorizontalAlignment = HorizontalAlignment.Stretch
- };
- grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
- grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
-
- for (var index = 0; index < fields.Length; index++)
- {
- grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
- grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
-
- var label = new TextBlock
- {
- Text = fields[index].Label,
- FontWeight = FontWeights.SemiBold,
- FontSize = 12
- };
- Grid.SetRow(label, index * 2);
- Grid.SetColumn(label, 0);
- grid.Children.Add(label);
-
- fields[index].TextBox.HorizontalAlignment = HorizontalAlignment.Stretch;
- Grid.SetRow(fields[index].TextBox, (index * 2) + 1);
- Grid.SetColumn(fields[index].TextBox, 0);
- grid.Children.Add(fields[index].TextBox);
-
- fields[index].Button.HorizontalAlignment = HorizontalAlignment.Left;
- fields[index].Button.VerticalAlignment = VerticalAlignment.Bottom;
- Grid.SetRow(fields[index].Button, (index * 2) + 1);
- Grid.SetColumn(fields[index].Button, 1);
- grid.Children.Add(fields[index].Button);
- }
-
- return grid;
- }
-
private static Grid TwoColumn(FrameworkElement left, FrameworkElement right)
{
var grid = new Grid { ColumnSpacing = 6 };