diff --git a/GenHub/GenHub.Core/Constants/ApiConstants.cs b/GenHub/GenHub.Core/Constants/ApiConstants.cs
index d0643258..33de7099 100644
--- a/GenHub/GenHub.Core/Constants/ApiConstants.cs
+++ b/GenHub/GenHub.Core/Constants/ApiConstants.cs
@@ -47,7 +47,7 @@ public static class ApiConstants
///
/// Format string for GitHub API Workflow Runs endpoint (owner, repo, branch).
///
- public const string GitHubApiWorkflowRunsFormat = "https://api.github.com/repos/{0}/{1}/actions/runs?status=success&branch={2}&per_page=10";
+ public const string GitHubApiWorkflowRunsFormat = "https://api.github.com/repos/{0}/{1}/actions/runs?status=success&branch={2}&event=push&per_page=10";
///
/// Format string for GitHub API Latest Workflow Runs endpoint (owner, repo).
diff --git a/GenHub/GenHub.Core/Models/Common/UserSettings.cs b/GenHub/GenHub.Core/Models/Common/UserSettings.cs
index 72583726..93d5dfff 100644
--- a/GenHub/GenHub.Core/Models/Common/UserSettings.cs
+++ b/GenHub/GenHub.Core/Models/Common/UserSettings.cs
@@ -111,6 +111,11 @@ public bool IsExplicitlySet(string propertyName)
///
public int? SubscribedPrNumber { get; set; }
+ ///
+ /// Gets or sets the subscribed branch name for update notifications (e.g. "development").
+ ///
+ public string? SubscribedBranch { get; set; }
+
///
/// Gets or sets the last dismissed update version to prevent repeated notifications.
///
@@ -143,6 +148,7 @@ public object Clone()
ApplicationDataPath = ApplicationDataPath,
UpdateChannel = UpdateChannel,
SubscribedPrNumber = SubscribedPrNumber,
+ SubscribedBranch = SubscribedBranch,
DismissedUpdateVersion = DismissedUpdateVersion,
ContentDirectories = ContentDirectories != null ? new List(ContentDirectories) : null,
GitHubDiscoveryRepositories = GitHubDiscoveryRepositories != null ? new List(GitHubDiscoveryRepositories) : null,
diff --git a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/ViewModels/MainViewModelTests.cs b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/ViewModels/MainViewModelTests.cs
index 764b0316..c7c2a97a 100644
--- a/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/ViewModels/MainViewModelTests.cs
+++ b/GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/ViewModels/MainViewModelTests.cs
@@ -69,6 +69,7 @@ public void Constructor_CreatesValidInstance()
mockProfileEditorFacade.Object,
mockVelopackUpdateManager.Object,
CreateProfileResourceService(),
+ mockNotificationService.Object,
mockLogger.Object);
// Assert
@@ -111,6 +112,7 @@ public void SelectTabCommand_SetsSelectedTab(NavigationTab tab)
mockProfileEditorFacade.Object,
mockVelopackUpdateManager.Object,
CreateProfileResourceService(),
+ mockNotificationService.Object,
mockLogger.Object);
vm.SelectTabCommand.Execute(tab);
Assert.Equal(tab, vm.SelectedTab);
@@ -148,6 +150,7 @@ public async Task ScanAndCreateProfilesAsync_CanBeCalled()
mockProfileEditorFacade.Object,
mockVelopackUpdateManager.Object,
CreateProfileResourceService(),
+ mockNotificationService.Object,
mockLogger.Object);
// Act & Assert
@@ -189,6 +192,7 @@ public async Task InitializeAsync_MultipleCallsAreSafe()
mockProfileEditorFacade.Object,
mockVelopackUpdateManager.Object,
CreateProfileResourceService(),
+ mockNotificationService.Object,
mockLogger.Object);
await vm.InitializeAsync(); // Should not throw
Assert.True(true);
@@ -229,6 +233,7 @@ public void CurrentTabViewModel_ReturnsCorrectViewModel(NavigationTab tab)
mockProfileEditorFacade.Object,
mockVelopackUpdateManager.Object,
CreateProfileResourceService(),
+ mockNotificationService.Object,
mockLogger.Object);
vm.SelectTabCommand.Execute(tab);
var currentViewModel = vm.CurrentTabViewModel;
diff --git a/GenHub/GenHub/Common/ViewModels/MainViewModel.cs b/GenHub/GenHub/Common/ViewModels/MainViewModel.cs
index 7885bc79..748e4811 100644
--- a/GenHub/GenHub/Common/ViewModels/MainViewModel.cs
+++ b/GenHub/GenHub/Common/ViewModels/MainViewModel.cs
@@ -4,14 +4,17 @@
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls;
+using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using GenHub.Core.Constants;
using GenHub.Core.Interfaces.Common;
using GenHub.Core.Interfaces.GameInstallations;
using GenHub.Core.Interfaces.GameProfiles;
+using GenHub.Core.Interfaces.Notifications;
using GenHub.Core.Models.Enums;
using GenHub.Core.Models.GameProfile;
+using GenHub.Core.Models.Notifications;
using GenHub.Features.AppUpdate.Interfaces;
using GenHub.Features.Downloads.ViewModels;
using GenHub.Features.GameProfiles.Services;
@@ -35,13 +38,13 @@ public partial class MainViewModel : ObservableObject, IDisposable
private readonly IProfileEditorFacade _profileEditorFacade;
private readonly IVelopackUpdateManager _velopackUpdateManager;
private readonly ProfileResourceService _profileResourceService;
+ private readonly INotificationService _notificationService;
private readonly CancellationTokenSource _initializationCts = new();
[ObservableProperty]
private NavigationTab _selectedTab = NavigationTab.GameProfiles;
- [ObservableProperty]
- private bool _hasUpdateAvailable;
+
///
/// Initializes a new instance of the class.
@@ -57,6 +60,7 @@ public partial class MainViewModel : ObservableObject, IDisposable
/// Profile editor facade for automatic profile creation.
/// The Velopack update manager for checking updates.
/// Service for accessing profile resources.
+ /// Service for showing notifications.
/// Logger instance.
public MainViewModel(
GameProfileLauncherViewModel gameProfilesViewModel,
@@ -70,6 +74,7 @@ public MainViewModel(
IProfileEditorFacade profileEditorFacade,
IVelopackUpdateManager velopackUpdateManager,
ProfileResourceService profileResourceService,
+ INotificationService notificationService,
ILogger? logger = null)
{
GameProfilesViewModel = gameProfilesViewModel;
@@ -83,6 +88,7 @@ public MainViewModel(
_profileEditorFacade = profileEditorFacade ?? throw new ArgumentNullException(nameof(profileEditorFacade));
_velopackUpdateManager = velopackUpdateManager ?? throw new ArgumentNullException(nameof(velopackUpdateManager));
_profileResourceService = profileResourceService ?? throw new ArgumentNullException(nameof(profileResourceService));
+ _notificationService = notificationService ?? throw new ArgumentNullException(nameof(notificationService));
_logger = logger;
// Load initial settings using unified configuration
@@ -181,30 +187,7 @@ public void SelectTab(NavigationTab tab)
SelectedTab = tab;
}
- ///
- /// Shows the update notification dialog.
- ///
- /// A task representing the asynchronous operation.
- [RelayCommand]
- public async Task ShowUpdateDialogAsync()
- {
- try
- {
- var mainWindow = GetMainWindow();
- if (mainWindow != null)
- {
- await GenHub.Features.AppUpdate.Views.UpdateNotificationWindow.ShowAsync(mainWindow);
- }
- else
- {
- _logger?.LogWarning("Cannot show update dialog - main window not found");
- }
- }
- catch (Exception ex)
- {
- _logger?.LogError(ex, "Failed to show update dialog");
- }
- }
+
///
/// Performs asynchronous initialization for the shell and all tabs.
@@ -372,115 +355,58 @@ private async Task CheckForUpdatesAsync(CancellationToken cancellationToken = de
try
{
- // Check if subscribed to a PR - if so, check for PR artifact updates instead
var settings = _userSettingsService.Get();
+
+ // Push settings to update manager (important context for other components)
if (settings.SubscribedPrNumber.HasValue)
{
- _logger?.LogDebug("User subscribed to PR #{PrNumber}, checking for PR artifact updates", settings.SubscribedPrNumber);
_velopackUpdateManager.SubscribedPrNumber = settings.SubscribedPrNumber;
+ }
- // Fetch PR list to populate artifact info
- var prs = await _velopackUpdateManager.GetOpenPullRequestsAsync(cancellationToken);
- var subscribedPr = prs.FirstOrDefault(p => p.Number == settings.SubscribedPrNumber);
+ // Check if subscribed to a specific branch
+ if (!string.IsNullOrEmpty(settings.SubscribedBranch))
+ {
+ _logger?.LogDebug("User subscribed to branch '{Branch}', checking for artifact updates", settings.SubscribedBranch);
+ _velopackUpdateManager.SubscribedBranch = settings.SubscribedBranch;
+
+ // Ensure PR number is cleared to avoid ambiguity
+ _velopackUpdateManager.SubscribedPrNumber = null;
+
+ var artifactUpdate = await _velopackUpdateManager.CheckForArtifactUpdatesAsync(cancellationToken);
- if (subscribedPr?.LatestArtifact != null)
+ if (artifactUpdate != null)
{
- // Compare versions (strip build metadata)
- var currentVersionBase = AppConstants.AppVersion.Split('+')[0];
- var prVersionBase = subscribedPr.LatestArtifact.Version.Split('+')[0];
+ var newVersionBase = artifactUpdate.Version.Split('+')[0];
+ var dismissedVersionBase = settings.DismissedUpdateVersion?.Split('+')[0];
- if (!string.Equals(prVersionBase, currentVersionBase, StringComparison.OrdinalIgnoreCase))
+ if (string.IsNullOrEmpty(dismissedVersionBase) ||
+ !string.Equals(newVersionBase, dismissedVersionBase, StringComparison.OrdinalIgnoreCase))
{
- // Check if this PR version was dismissed
- var dismissedVersionBase = settings.DismissedUpdateVersion?.Split('+')[0];
+ _logger?.LogInformation("Branch '{Branch}' artifact update available: {Version}", settings.SubscribedBranch, newVersionBase);
- if (string.IsNullOrEmpty(dismissedVersionBase) ||
- !string.Equals(prVersionBase, dismissedVersionBase, StringComparison.OrdinalIgnoreCase))
- {
- _logger?.LogInformation("PR #{PrNumber} artifact update available: {Version}", subscribedPr.Number, prVersionBase);
- HasUpdateAvailable = true;
- return;
- }
- else
+ await Dispatcher.UIThread.InvokeAsync(() =>
{
- _logger?.LogDebug("PR #{PrNumber} artifact update {Version} was dismissed", subscribedPr.Number, prVersionBase);
- HasUpdateAvailable = false;
- return;
- }
+ _notificationService.Show(new NotificationMessage(
+ NotificationType.Info,
+ "Update Available",
+ $"A new version ({newVersionBase}) is available on branch '{settings.SubscribedBranch}'.",
+ null,
+ "View Updates",
+ () => { SettingsViewModel.OpenUpdateWindowCommand.Execute(null); }));
+ });
+ return;
}
else
{
- _logger?.LogDebug("Already on latest PR #{PrNumber} artifact version", subscribedPr.Number);
- HasUpdateAvailable = false;
+ _logger?.LogDebug("Branch '{Branch}' artifact update {Version} was dismissed", settings.SubscribedBranch, newVersionBase);
return;
}
}
- else
- {
- _logger?.LogDebug("PR #{PrNumber} has no artifacts or PR not found", settings.SubscribedPrNumber);
-
- // Fall through to check main branch updates
- }
- }
-
- // Check main branch updates (if not subscribed to PR or PR has no artifacts)
- var updateInfo = await _velopackUpdateManager.CheckForUpdatesAsync(cancellationToken);
-
- // Check both UpdateInfo (from installed app) and GitHub API flag (works in debug too)
- var hasUpdate = updateInfo != null || _velopackUpdateManager.HasUpdateAvailableFromGitHub;
-
- if (hasUpdate)
- {
- string? latestVersion = null;
-
- if (updateInfo != null)
- {
- latestVersion = updateInfo.TargetFullRelease.Version.ToString();
- _logger?.LogInformation("Update available: {Current} → {Latest}", AppConstants.AppVersion, latestVersion);
- }
- else if (_velopackUpdateManager.LatestVersionFromGitHub != null)
- {
- latestVersion = _velopackUpdateManager.LatestVersionFromGitHub;
- _logger?.LogInformation("Update available from GitHub API: {Version}", latestVersion);
- }
-
- // Strip build metadata for comparison (everything after '+')
- var latestVersionBase = latestVersion?.Split('+')[0];
- var currentVersionBase = AppConstants.AppVersion.Split('+')[0];
-
- // Check if this version was dismissed by the user
- var settings2 = _userSettingsService.Get();
- var dismissedVersionBase = settings2.DismissedUpdateVersion?.Split('+')[0];
-
- if (!string.IsNullOrEmpty(latestVersionBase) &&
- string.Equals(latestVersionBase, dismissedVersionBase, StringComparison.OrdinalIgnoreCase))
- {
- _logger?.LogDebug("Update {Version} was dismissed by user, hiding notification", latestVersionBase);
- HasUpdateAvailable = false;
- }
-
- // Also check if we're already on this version (ignoring build metadata)
- else if (!string.IsNullOrEmpty(latestVersionBase) &&
- string.Equals(latestVersionBase, currentVersionBase, StringComparison.OrdinalIgnoreCase))
- {
- _logger?.LogDebug("Already on version {Version} (ignoring build metadata), hiding notification", latestVersionBase);
- HasUpdateAvailable = false;
- }
- else
- {
- HasUpdateAvailable = true;
- }
- }
- else
- {
- _logger?.LogDebug("No updates available");
- HasUpdateAvailable = false;
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "Exception in CheckForUpdatesAsync");
- HasUpdateAvailable = false;
}
}
diff --git a/GenHub/GenHub/Common/Views/MainView.axaml b/GenHub/GenHub/Common/Views/MainView.axaml
index e7ad4d64..dd66a99e 100644
--- a/GenHub/GenHub/Common/Views/MainView.axaml
+++ b/GenHub/GenHub/Common/Views/MainView.axaml
@@ -87,29 +87,7 @@
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/GenHub/GenHub/Features/AppUpdate/Views/UpdateNotificationWindow.axaml b/GenHub/GenHub/Features/AppUpdate/Views/UpdateNotificationWindow.axaml
index 69b602e1..1448a6f8 100644
--- a/GenHub/GenHub/Features/AppUpdate/Views/UpdateNotificationWindow.axaml
+++ b/GenHub/GenHub/Features/AppUpdate/Views/UpdateNotificationWindow.axaml
@@ -3,8 +3,8 @@
xmlns:views="using:GenHub.Features.AppUpdate.Views"
xmlns:vm="using:GenHub.Features.AppUpdate.ViewModels"
x:Class="GenHub.Features.AppUpdate.Views.UpdateNotificationWindow"
- Width="580" Height="800"
- MinWidth="500" MinHeight="580"
+ Width="900" Height="800"
+ MinWidth="750" MinHeight="580"
Title="GenHub Updates"
Icon="/Assets/Icons/generalshub-icon.png"
WindowStartupLocation="CenterScreen"
@@ -102,4 +102,4 @@
-
\ No newline at end of file
+
diff --git a/GenHub/GenHub/Features/Downloads/Views/DownloadsView.axaml b/GenHub/GenHub/Features/Downloads/Views/DownloadsView.axaml
index f31ee7a3..84a0090b 100644
--- a/GenHub/GenHub/Features/Downloads/Views/DownloadsView.axaml
+++ b/GenHub/GenHub/Features/Downloads/Views/DownloadsView.axaml
@@ -91,29 +91,7 @@
Foreground="#AAAAAA"
HorizontalAlignment="Center" />
-
-
-
-
-
-
+
diff --git a/GenHub/GenHub/Features/Settings/ViewModels/SettingsViewModel.cs b/GenHub/GenHub/Features/Settings/ViewModels/SettingsViewModel.cs
index f2c71abf..fd6d531c 100644
--- a/GenHub/GenHub/Features/Settings/ViewModels/SettingsViewModel.cs
+++ b/GenHub/GenHub/Features/Settings/ViewModels/SettingsViewModel.cs
@@ -181,6 +181,9 @@ public partial class SettingsViewModel : ObservableObject, IDisposable
[ObservableProperty]
private UpdateChannel _selectedUpdateChannel = UpdateChannel.Prerelease;
+ [ObservableProperty]
+ private string _subscribedBranchInput = string.Empty;
+
[ObservableProperty]
private string _gitHubPatInput = string.Empty;
@@ -498,6 +501,7 @@ private void LoadSettings()
CasRootPath = settings.CasConfiguration.CasRootPath;
EnableAutomaticGc = settings.CasConfiguration.EnableAutomaticGc;
SelectedUpdateChannel = settings.UpdateChannel;
+ SubscribedBranchInput = settings.SubscribedBranch ?? string.Empty;
MaxCacheSizeGB = settings.CasConfiguration.MaxCacheSizeBytes / ConversionConstants.BytesPerGigabyte;
CasMaxConcurrentOperations = settings.CasConfiguration.MaxConcurrentOperations;
CasVerifyIntegrity = settings.CasConfiguration.VerifyIntegrity;
@@ -539,6 +543,7 @@ private async Task SaveSettings()
settings.EnableDetailedLogging = EnableDetailedLogging;
settings.DefaultWorkspaceStrategy = DefaultWorkspaceStrategy;
settings.UpdateChannel = SelectedUpdateChannel;
+ settings.SubscribedBranch = string.IsNullOrWhiteSpace(SubscribedBranchInput) ? null : SubscribedBranchInput;
settings.DownloadBufferSize = (int)(DownloadBufferSizeKB * ConversionConstants.BytesPerKilobyte); // Convert KB to bytes
settings.DownloadTimeoutSeconds = DownloadTimeoutSeconds;
settings.DownloadUserAgent = DownloadUserAgent;
diff --git a/GenHub/GenHub/Features/Settings/Views/SettingsView.axaml b/GenHub/GenHub/Features/Settings/Views/SettingsView.axaml
index a06cf468..ea2a7286 100644
--- a/GenHub/GenHub/Features/Settings/Views/SettingsView.axaml
+++ b/GenHub/GenHub/Features/Settings/Views/SettingsView.axaml
@@ -287,7 +287,7 @@
-
+
-
-
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -748,10 +679,16 @@
MinWidth="200" />
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/GenHub/GenHub/Infrastructure/DependencyInjection/SharedViewModelModule.cs b/GenHub/GenHub/Infrastructure/DependencyInjection/SharedViewModelModule.cs
index e644b9d2..58dda1c8 100644
--- a/GenHub/GenHub/Infrastructure/DependencyInjection/SharedViewModelModule.cs
+++ b/GenHub/GenHub/Infrastructure/DependencyInjection/SharedViewModelModule.cs
@@ -1,5 +1,6 @@
using GenHub.Common.ViewModels;
using GenHub.Core.Interfaces.Common;
+using GenHub.Core.Interfaces.GitHub;
using GenHub.Core.Interfaces.GameInstallations;
using GenHub.Core.Interfaces.GameProfiles;
using GenHub.Core.Interfaces.Manifest;
@@ -46,7 +47,8 @@ public static IServiceCollection AddSharedViewModelModule(this IServiceCollectio
sp.GetRequiredService(),
sp.GetRequiredService(),
sp.GetRequiredService(),
- sp.GetRequiredService()));
+ sp.GetRequiredService(),
+ sp.GetService()));
services.AddSingleton();
// Register PublisherCardViewModel as transient
diff --git a/build-release.ps1 b/build-release.ps1
new file mode 100644
index 00000000..7a0179b7
--- /dev/null
+++ b/build-release.ps1
@@ -0,0 +1,93 @@
+# GenHub Test Release Build Script
+# This script builds and packages GenHub as version 0.0.0 for local testing.
+
+$ErrorActionPreference = "Stop"
+
+$version = "0.0.1"
+$configuration = "Release"
+$runtime = "win-x64"
+$projectPath = "GenHub/GenHub.Windows/GenHub.Windows.csproj"
+$publishDir = "win-publish-test"
+$outputDir = "Releases"
+$appName = "GenHub"
+$authors = "Community Outpost"
+$iconPath = "GenHub/GenHub/Assets/Icons/generalshub.ico"
+
+Write-Host "--- GenHub Test Build v$version ---" -ForegroundColor Cyan
+
+# 1. Cleanup
+if (Test-Path $publishDir) {
+ Write-Host "Cleaning up old publish directory..."
+ Remove-Item -Path $publishDir -Recurse -Force
+}
+if (Test-Path $outputDir) {
+ Write-Host "Cleaning up old output directory..."
+ Remove-Item -Path $outputDir -Recurse -Force
+}
+
+# 2. Check for Velopack CLI (vpk)
+Write-Host "Checking for Velopack CLI (vpk)..."
+try {
+ & vpk --help | Out-Null
+} catch {
+ Write-Error "Velopack CLI (vpk) not found. Please install it with: dotnet tool install -g vpk"
+}
+
+# 3. Publish Windows App
+Write-Host "Publishing Windows application..." -ForegroundColor Green
+dotnet publish $projectPath `
+ -c $configuration `
+ -r $runtime `
+ --self-contained true `
+ -p:Version=$version `
+ -p:BuildChannel="Test" `
+ -o $publishDir
+
+if ($LASTEXITCODE -ne 0) {
+ Write-Error "Dotnet publish failed."
+}
+
+# 4. Create Velopack Package
+Write-Host "Creating Velopack package..." -ForegroundColor Green
+$tempPackDir = "temp-pack-output"
+if (Test-Path $tempPackDir) { Remove-Item -Path $tempPackDir -Recurse -Force }
+New-Item -ItemType Directory -Path $tempPackDir | Out-Null
+
+if (!(Test-Path $outputDir)) {
+ New-Item -ItemType Directory -Path $outputDir | Out-Null
+}
+
+& vpk pack `
+ --packId $appName `
+ --packVersion $version `
+ --packDir $publishDir `
+ --mainExe "$appName.Windows.exe" `
+ --packTitle $appName `
+ --packAuthors $authors `
+ --icon $iconPath `
+ --outputDir $tempPackDir
+
+if ($LASTEXITCODE -ne 0) {
+ Write-Error "Velopack pack failed."
+}
+
+# 5. Extract only Setup.exe and Cleanup
+Write-Host "Cleaning up and extracting Setup.exe..." -ForegroundColor Yellow
+$setupExe = Get-ChildItem -Path $tempPackDir -Filter "*Setup.exe" | Select-Object -First 1
+if ($null -ne $setupExe) {
+ Move-Item -Path $setupExe.FullName -Destination "$outputDir\GenHub-Setup.exe" -Force
+ Write-Host "Success: GenHub-Setup.exe is ready in $outputDir" -ForegroundColor Green
+} else {
+ Write-Error "Could not find Setup.exe in Velopack output."
+}
+
+# Cleanup temporary directories
+Remove-Item -Path $tempPackDir -Recurse -Force
+Remove-Item -Path $publishDir -Recurse -Force
+
+Write-Host ""
+Write-Host "--- Build Complete! ---" -ForegroundColor Cyan
+Write-Host "Installer: $outputDir\GenHub-Setup.exe"
+Write-Host "Version: $version"
+Write-Host ""
+