Skip to content

Commit 3576a48

Browse files
Add daily DesktopManager app MSI packaging
* feat(app): ✨ Add daily app MSI packaging * fix(app): 🐛 Address tray startup review feedback
1 parent 0bff106 commit 3576a48

13 files changed

Lines changed: 434 additions & 24 deletions

.agents/skills/desktopmanager-build/SKILL.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,35 @@ Use this skill when the task touches how DesktopManager is built, packaged, or p
1515
`.\Build\Build-Project.ps1 -Plan`
1616
3. For PowerShell module-only work, use:
1717
`.\Build\Build-Module.ps1 -SkipInstall`
18-
4. For CLI-only publish verification, use:
18+
4. For CLI/app publish verification, use:
1919
`.\Build\Build-Project.ps1 -Build:$false -BuildModule:$false`
20-
5. When checking a specific runtime publish, pass:
20+
5. For the daily WinUI tray app without installers, use:
21+
`.\Build\Build-DesktopManagerApp.ps1`
22+
6. For the generated DesktopManager app MSI, use:
23+
`.\Build\Build-DesktopManagerApp-MSI.ps1`
24+
The MSI wrapper publishes `DesktopManager.App` as `PortableCompat`.
25+
7. When checking a specific runtime publish, pass:
2126
`-Runtimes win-x64`
2227

2328
## Decision Rules
2429

2530
- Treat `Build/Build-Project.ps1` as the primary repo build and release entrypoint.
2631
- Prefer changing `Build/project.build.json` and `powerforge.dotnetpublish.json` over hardcoding publish behavior in ad-hoc scripts.
2732
- Prefer `Build/Build-Module.ps1` for PowerShell packaging behavior instead of editing checked-in artefacts by hand.
33+
- Prefer the app/MSI wrapper scripts for daily desktop app packaging instead of invoking generated WiX files directly.
2834
- Verify the resulting behaviour from the real surfaces:
29-
NuGet/library, PowerShell import, CLI help, and `desktopmanager mcp serve`.
35+
NuGet/library, PowerShell import, CLI help, `desktopmanager mcp serve`, `DesktopManager.App.exe`, and MSI plan/build output.
3036
- Keep docs aligned with the actual build flow:
3137
`Docs/Build.Runbook.md`, `README.MD`, and repo-local skills.
3238
- When packaging or publish output changes, check the expected output locations before concluding:
33-
`Artefacts/ProjectBuild`, `Artefacts/Unpacked`, `Artefacts/Packed`, and `Artefacts/PowerForge/DesktopManager`.
39+
`Artefacts/ProjectBuild`, `Artefacts/Unpacked`, `Artefacts/Packed`, `Artefacts/PowerForge/DesktopManager`, `Artefacts/PowerForge/DesktopManager.App`, and `Artefacts/PowerForge/Msi/DesktopManager.App`.
3440

3541
## Reference Files
3642

3743
- `Build/Build-Project.ps1`
3844
- `Build/Build-Module.ps1`
45+
- `Build/Build-DesktopManagerApp.ps1`
46+
- `Build/Build-DesktopManagerApp-MSI.ps1`
3947
- `Build/project.build.json`
4048
- `powerforge.dotnetpublish.json`
4149
- `Docs/Build.Runbook.md`
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
param(
2+
[string] $ConfigPath = "$PSScriptRoot\..\powerforge.dotnetpublish.json",
3+
[ValidateSet('win-x64')]
4+
[string[]] $Runtimes = @('win-x64'),
5+
[ValidateSet('net10.0-windows10.0.19041.0')]
6+
[string[]] $Frameworks = @('net10.0-windows10.0.19041.0'),
7+
[ValidateSet('PortableCompat')]
8+
[string[]] $Styles = @('PortableCompat'),
9+
[switch] $Plan,
10+
[switch] $Validate,
11+
[switch] $SkipRestore,
12+
[switch] $SkipBuild
13+
)
14+
15+
$ErrorActionPreference = 'Stop'
16+
17+
Import-Module PSPublishModule -Force -ErrorAction Stop
18+
19+
$invokeParams = @{
20+
ConfigPath = $ConfigPath
21+
Target = @('DesktopManager.App')
22+
Runtimes = $Runtimes
23+
Frameworks = $Frameworks
24+
Styles = $Styles
25+
}
26+
27+
if ($Plan) { $invokeParams.Plan = $true }
28+
if ($Validate) { $invokeParams.Validate = $true }
29+
if ($SkipRestore) { $invokeParams.SkipRestore = $true }
30+
if ($SkipBuild) { $invokeParams.SkipBuild = $true }
31+
32+
Invoke-DotNetPublish @invokeParams

Build/Build-DesktopManagerApp.ps1

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
param(
2+
[string] $ConfigPath = "$PSScriptRoot\..\powerforge.dotnetpublish.json",
3+
[ValidateSet('win-x64')]
4+
[string[]] $Runtimes = @('win-x64'),
5+
[ValidateSet('net10.0-windows10.0.19041.0')]
6+
[string[]] $Frameworks = @('net10.0-windows10.0.19041.0'),
7+
[ValidateSet('PortableCompat')]
8+
[string[]] $Styles = @('PortableCompat'),
9+
[switch] $Plan,
10+
[switch] $Validate,
11+
[switch] $SkipRestore,
12+
[switch] $SkipBuild
13+
)
14+
15+
$ErrorActionPreference = 'Stop'
16+
17+
Import-Module PSPublishModule -Force -ErrorAction Stop
18+
19+
$invokeParams = @{
20+
ConfigPath = $ConfigPath
21+
Target = @('DesktopManager.App')
22+
Runtimes = $Runtimes
23+
Frameworks = $Frameworks
24+
Styles = $Styles
25+
SkipInstallers = $true
26+
}
27+
28+
if ($Plan) { $invokeParams.Plan = $true }
29+
if ($Validate) { $invokeParams.Validate = $true }
30+
if ($SkipRestore) { $invokeParams.SkipRestore = $true }
31+
if ($SkipBuild) { $invokeParams.SkipBuild = $true }
32+
33+
Invoke-DotNetPublish @invokeParams

Build/Build-Project.ps1

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,67 @@
11
param(
22
[string] $ConfigPath = "$PSScriptRoot\project.build.json",
3+
[string] $DotNetPublishConfigPath = "$PSScriptRoot\..\powerforge.dotnetpublish.json",
34
[Nullable[bool]] $UpdateVersions,
45
[Nullable[bool]] $Build,
6+
[bool] $BuildModule = $true,
7+
[bool] $PublishTools = $true,
58
[Nullable[bool]] $PublishNuget = $false,
69
[Nullable[bool]] $PublishGitHub = $false,
7-
[Nullable[bool]] $Plan,
8-
[string] $PlanPath
10+
[switch] $Plan,
11+
[string] $PlanPath,
12+
[string[]] $Target = @(),
13+
[string[]] $Runtimes = @(),
14+
[string[]] $Frameworks = @(),
15+
[ValidateSet('Portable', 'PortableCompat', 'PortableSize', 'FrameworkDependent', 'AotSpeed', 'AotSize')]
16+
[string[]] $Styles = @(),
17+
[switch] $SkipInstallers,
18+
[switch] $Validate,
19+
[switch] $SkipRestore,
20+
[switch] $SkipBuild
921
)
1022

1123
Import-Module PSPublishModule -Force -ErrorAction Stop
1224

25+
function Invoke-DesktopManagerDotNetPublish {
26+
$dotNetPublishParams = @{
27+
ConfigPath = $DotNetPublishConfigPath
28+
}
29+
30+
if ($Target.Count -gt 0) { $dotNetPublishParams.Target = $Target }
31+
if ($Runtimes.Count -gt 0) { $dotNetPublishParams.Runtimes = $Runtimes }
32+
if ($Frameworks.Count -gt 0) { $dotNetPublishParams.Frameworks = $Frameworks }
33+
if ($Styles.Count -gt 0) { $dotNetPublishParams.Styles = $Styles }
34+
if ($Plan) { $dotNetPublishParams.Plan = $true }
35+
if ($Validate) { $dotNetPublishParams.Validate = $true }
36+
if ($SkipInstallers) { $dotNetPublishParams.SkipInstallers = $true }
37+
if ($SkipRestore) { $dotNetPublishParams.SkipRestore = $true }
38+
if ($SkipBuild) { $dotNetPublishParams.SkipBuild = $true }
39+
40+
Invoke-DotNetPublish @dotNetPublishParams
41+
}
42+
43+
$dotNetPublishInvoked = $false
44+
if ($Plan -and $PublishTools) {
45+
Invoke-DesktopManagerDotNetPublish
46+
$dotNetPublishInvoked = $true
47+
}
48+
1349
$invokeParams = @{
1450
ConfigPath = $ConfigPath
1551
}
1652
if ($null -ne $UpdateVersions) { $invokeParams.UpdateVersions = $UpdateVersions }
1753
if ($null -ne $Build) { $invokeParams.Build = $Build }
1854
if ($null -ne $PublishNuget) { $invokeParams.PublishNuget = $PublishNuget }
1955
if ($null -ne $PublishGitHub) { $invokeParams.PublishGitHub = $PublishGitHub }
20-
if ($null -ne $Plan) { $invokeParams.Plan = $Plan }
56+
if ($Plan) { $invokeParams.Plan = $true }
2157
if ($PlanPath) { $invokeParams.PlanPath = $PlanPath }
2258

2359
Invoke-ProjectBuild @invokeParams
60+
61+
if ($BuildModule -and -not $Plan) {
62+
& (Join-Path $PSScriptRoot 'Build-Module.ps1') -SkipInstall
63+
}
64+
65+
if ($PublishTools -and -not $dotNetPublishInvoked) {
66+
Invoke-DesktopManagerDotNetPublish
67+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{\rtf1\ansi\deff0
2+
{\fonttbl{\f0 Segoe UI;}}
3+
\f0\fs20
4+
DesktopManager\par
5+
\par
6+
Copyright (c) 2011 - 2026 Przemyslaw Klys @ Evotec. All rights reserved.\par
7+
\par
8+
This installer is provided for DesktopManager daily desktop use. Install it only on machines where you are allowed to run Evotec desktop automation tooling.\par
9+
\par
10+
DesktopManager can inspect and automate desktop windows, controls, input, monitor state, screenshots, and related local desktop resources. Review the app settings before enabling startup or unattended workflows.\par
11+
}

Docs/Build.Runbook.md

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# DesktopManager Build Runbook
22

3-
DesktopManager now uses one repo entrypoint for package, module, and CLI outputs:
3+
DesktopManager uses one repo entrypoint for package, module, CLI, app, and installer outputs:
44

55
```powershell
66
.\Build\Build-Project.ps1
@@ -9,13 +9,17 @@ DesktopManager now uses one repo entrypoint for package, module, and CLI outputs
99
## Build Surfaces
1010

1111
- `Build/Build-Project.ps1`
12-
Orchestrates repository package build, PowerShell module build, and CLI publish.
12+
Orchestrates repository package build, PowerShell module build, CLI publish, app publish, and installer publish.
13+
- `Build/Build-DesktopManagerApp.ps1`
14+
Publishes the daily-use WinUI tray app without building installers.
15+
- `Build/Build-DesktopManagerApp-MSI.ps1`
16+
Publishes the WinUI tray app and builds the generated MSI installer.
1317
- `Build/project.build.json`
1418
Controls `Invoke-ProjectBuild` for the `DesktopManager` NuGet package and GitHub/NuGet release settings.
1519
- `Build/Build-Module.ps1`
1620
Builds the PowerShell module artefacts using `Invoke-ModuleBuild`.
1721
- `powerforge.dotnetpublish.json`
18-
Controls `Invoke-DotNetPublish` for the `desktopmanager.exe` publish output.
22+
Controls `Invoke-DotNetPublish` for `desktopmanager.exe`, `DesktopManager.App.exe`, the app zip, and the generated MSI.
1923

2024
## Common Commands
2125

@@ -43,12 +47,42 @@ Build module only:
4347
.\Build\Build-Module.ps1 -SkipInstall
4448
```
4549

46-
Build CLI output only:
50+
Build CLI and app outputs only:
4751

4852
```powershell
4953
.\Build\Build-Project.ps1 -Build:$false -BuildModule:$false
5054
```
5155

56+
Build the daily tray app portable output only:
57+
58+
```powershell
59+
.\Build\Build-DesktopManagerApp.ps1
60+
```
61+
62+
Plan the app MSI without compiling:
63+
64+
```powershell
65+
.\Build\Build-DesktopManagerApp-MSI.ps1 -Plan
66+
```
67+
68+
Build the app MSI:
69+
70+
```powershell
71+
.\Build\Build-DesktopManagerApp-MSI.ps1
72+
```
73+
74+
Install it interactively:
75+
76+
```powershell
77+
msiexec.exe /i .\Artefacts\PowerForge\Msi\DesktopManager.App\win-x64\net10.0-windows10.0.19041.0\PortableCompat\output\DesktopManager.msi
78+
```
79+
80+
Use silent install only when automation intentionally asks for it:
81+
82+
```powershell
83+
msiexec.exe /i .\Artefacts\PowerForge\Msi\DesktopManager.App\win-x64\net10.0-windows10.0.19041.0\PortableCompat\output\DesktopManager.msi /qn
84+
```
85+
5286
Publish the NuGet package using the repo config:
5387

5488
```powershell
@@ -64,7 +98,7 @@ Publish the GitHub release asset using the repo config:
6498
Publish a specific CLI runtime:
6599

66100
```powershell
67-
.\Build\Build-Project.ps1 -Build:$false -BuildModule:$false -Runtimes win-x64
101+
.\Build\Build-Project.ps1 -Build:$false -BuildModule:$false -Target DesktopManager.Cli.net10 -Runtimes win-x64 -SkipInstallers
68102
```
69103

70104
## Output Locations
@@ -76,12 +110,21 @@ Publish a specific CLI runtime:
76110
`Artefacts/Packed`
77111
- CLI publish output and manifests:
78112
`Artefacts/PowerForge/DesktopManager`
113+
- Daily app publish output and zip:
114+
`Artefacts/PowerForge/DesktopManager.App`
115+
- Daily app MSI staging, generated WiX authoring, and installer output:
116+
`Artefacts/PowerForge/Msi/DesktopManager.App`
79117

80118
## Notes
81119

82120
- `Build-Project.ps1` is the only package and release entrypoint in this repo.
83121
- `Build-Project.ps1 -Plan` skips the module build execution because the module path does not expose the same standalone plan surface here.
84122
- The CLI publish targets package `Sources/DesktopManager.Cli/DesktopManager.Cli.csproj` as `desktopmanager.exe` for both `net8.0-windows` and `net10.0-windows`.
123+
- The daily app publish target packages `Sources/DesktopManager.App/DesktopManager.App.csproj` as an unpackaged, self-contained WinUI app under `DesktopManager.App`.
124+
- The WinUI app is not forced into NativeAOT. The nested `DesktopManager.HotkeyHost` remains NativeAOT, while the app uses a self-contained publish profile that avoids WinAppSDK single-file and NativeAOT limitations.
125+
- The generated MSI uses PowerForge/WiX installer UI, creates a Start Menu shortcut, and records the install path under `HKLM\Software\Evotec\DesktopManager`.
126+
- DesktopManager autostart stays a per-user app setting. The app writes the current user's Run key with `--minimized`, so Windows sign-in starts the tray runtime without opening the main window.
127+
- The MSI output, manifest, and checksums are the intended inputs for later winget release work. Microsoft Store/MSIX packaging should stay in PowerForge config when that lane is added.
85128
- The CLI includes the MCP server entrypoint exposed by:
86129

87130
```powershell

README.MD

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ Additional operator-focused docs for the newer CLI and MCP surfaces:
7373
| PowerShell module | Scripts, operators, admin workflows, repeatable task automation | `Install-Module DesktopManager` |
7474
| CLI executable | Manual use, shell automation, JSON output, and MCP hosting | `desktopmanager.exe` |
7575
| MCP server | Agent-driven automation over stdio with an inspect-first safety model | `desktopmanager mcp serve` |
76+
| Windows tray app | Daily hotkeys, monitor bindings, HDR controls, tray runtime, and per-user startup | `DesktopManager.App.exe` |
7677

7778
## How It Fits Together
7879

@@ -84,6 +85,7 @@ flowchart LR
8485
C["PowerShell module"] --> B
8586
D["desktopmanager.exe CLI"] --> B
8687
E["MCP server"] --> D
88+
J["DesktopManager.App tray app"] --> B
8789
B --> F["Windows desktop APIs"]
8890
F --> G["Monitors / wallpapers / brightness"]
8991
F --> H["Windows / controls / input"]
@@ -105,6 +107,7 @@ The practical way to think about this:
105107
| ---- | ---------------------- |
106108
| Add DesktopManager features to your own code | Reference the `DesktopManager` NuGet package |
107109
| Run manual desktop operations from a terminal | `desktopmanager.exe` |
110+
| Run DesktopManager every day from the tray | `DesktopManager.App.exe` or the DesktopManager MSI |
108111
| Use it from PowerShell scripts | `DesktopManager` PowerShell module |
109112
| Connect an agent/tooling layer | `desktopmanager mcp serve` |
110113
| Understand the overall wiring | [Docs/DesktopManager.Architecture.md](Docs/DesktopManager.Architecture.md) |
@@ -118,10 +121,12 @@ flowchart LR
118121
B -->|"PowerShell"| D["Cmdlets"]
119122
B -->|"CLI"| E["desktopmanager.exe"]
120123
B -->|"MCP"| F["desktopmanager mcp serve"]
124+
B -->|"Tray app"| J["DesktopManager.App.exe"]
121125
122126
D --> C
123127
E --> G["DesktopOperations"]
124128
F --> G
129+
J --> C
125130
G --> C
126131
C --> H["Win32, UI Automation, monitor, screenshot, and input services"]
127132
```
@@ -260,6 +265,16 @@ For using in PowerShell you can install it from PowerShell Gallery
260265
Install-Module DesktopManager -Force -Verbose
261266
```
262267

268+
For the daily Windows tray app, use the DesktopManager MSI release artifact when available. The installer creates a Start Menu shortcut, and the app's Startup toggle registers the current user for minimized tray startup.
269+
270+
From source, the app and MSI are built through PowerForge:
271+
272+
```powershell
273+
.\Build\Build-DesktopManagerApp.ps1
274+
.\Build\Build-DesktopManagerApp-MSI.ps1
275+
msiexec.exe /i .\Artefacts\PowerForge\Msi\DesktopManager.App\win-x64\net10.0-windows10.0.19041.0\PortableCompat\output\DesktopManager.msi
276+
```
277+
263278
### Verified mutation examples
264279

265280
When you want more than "the call did not throw", prefer `-Verify` together with `-PassThru` so the cmdlet returns the observed postcondition.

Sources/DesktopManager.App/App.xaml.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,30 @@ protected override void OnLaunched(LaunchActivatedEventArgs args) {
2727
return;
2828
}
2929

30+
bool startMinimized = ShouldStartMinimized(args.Arguments, Environment.GetCommandLineArgs());
3031
_window = new MainWindow();
3132
_window.Activate();
33+
if (startMinimized && _window is MainWindow mainWindow) {
34+
mainWindow.HideToTrayAfterLaunch();
35+
}
36+
}
37+
38+
private static bool ShouldStartMinimized(string? launchArguments, string[] processArguments) {
39+
if (processArguments.Any(IsMinimizedArgument)) {
40+
return true;
41+
}
42+
43+
if (string.IsNullOrWhiteSpace(launchArguments)) {
44+
return false;
45+
}
46+
47+
string[] parts = launchArguments.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
48+
return parts.Any(IsMinimizedArgument);
49+
}
50+
51+
private static bool IsMinimizedArgument(string argument) {
52+
return string.Equals(argument, "--minimized", StringComparison.OrdinalIgnoreCase) ||
53+
string.Equals(argument, "--minimized-to-tray", StringComparison.OrdinalIgnoreCase) ||
54+
string.Equals(argument, "/minimized", StringComparison.OrdinalIgnoreCase);
3255
}
3356
}

Sources/DesktopManager.App/DesktopManager.App.csproj

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,13 @@
2525
<ProjectReference Include="..\DesktopManager.App.Core\DesktopManager.App.Core.csproj" />
2626
</ItemGroup>
2727

28+
<PropertyGroup>
29+
<DesktopManagerHotkeyHostTargets Condition="'$(NoRestore)' == 'true' Or '$(Restore)' == 'false'">Publish</DesktopManagerHotkeyHostTargets>
30+
<DesktopManagerHotkeyHostTargets Condition="'$(DesktopManagerHotkeyHostTargets)' == ''">Restore;Publish</DesktopManagerHotkeyHostTargets>
31+
</PropertyGroup>
32+
2833
<Target Name="PublishHotkeyHost" BeforeTargets="Build;Publish">
29-
<MSBuild Projects="$(MSBuildProjectDirectory)\..\DesktopManager.HotkeyHost\DesktopManager.HotkeyHost.csproj" Targets="Publish" Properties="Configuration=$(Configuration);TargetFramework=$(DesktopManagerNet10WindowsTfm);RuntimeIdentifier=win-x64" />
34+
<MSBuild Projects="$(MSBuildProjectDirectory)\..\DesktopManager.HotkeyHost\DesktopManager.HotkeyHost.csproj" Targets="$(DesktopManagerHotkeyHostTargets)" Properties="Configuration=$(Configuration);TargetFramework=$(DesktopManagerNet10WindowsTfm);RuntimeIdentifier=win-x64" />
3035
</Target>
3136

3237
<Target Name="CopyHotkeyHostDependencies" AfterTargets="Build">

0 commit comments

Comments
 (0)