diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7800ede..4336ce9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,16 +14,20 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Setup .NET 8 + - name: Setup .NET 10 uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.0.x' + dotnet-version: '10.0.x' - name: Restore dependencies - run: dotnet restore WindowSeat.sln + run: | + dotnet restore src/WindowSeat.App/WindowSeat.App.csproj + dotnet restore src/WindowSeat.Tests/WindowSeat.Tests.csproj - name: Build (Release) - run: dotnet build WindowSeat.sln --configuration Release --no-restore + run: | + dotnet build src/WindowSeat.App/WindowSeat.App.csproj --configuration Release --no-restore + dotnet build src/WindowSeat.Tests/WindowSeat.Tests.csproj --configuration Release --no-restore - name: Run tests run: dotnet test src/WindowSeat.Tests/WindowSeat.Tests.csproj --configuration Release --no-build --verbosity normal --logger trx --results-directory TestResults diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f4debc5..3d0f49c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,10 +13,13 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Setup .NET 8 + - name: Setup .NET 10 uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.0.x' + dotnet-version: '10.0.x' + + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v2 - name: Extract version from tag id: version @@ -27,14 +30,14 @@ jobs: echo "VERSION=$version" >> $env:GITHUB_OUTPUT - name: Restore dependencies - run: dotnet restore WindowSeat.sln + run: | + dotnet restore src/WindowSeat.App/WindowSeat.App.csproj + dotnet restore src/WindowSeat.Tests/WindowSeat.Tests.csproj - name: Build (Release) - run: > - dotnet build WindowSeat.sln - --configuration Release - --no-restore - /p:Version=${{ steps.version.outputs.VERSION }} + run: | + dotnet build src/WindowSeat.App/WindowSeat.App.csproj --configuration Release --no-restore /p:Version=${{ steps.version.outputs.VERSION }} + dotnet build src/WindowSeat.Tests/WindowSeat.Tests.csproj --configuration Release --no-restore - name: Run tests run: > @@ -52,16 +55,40 @@ jobs: --output publish/ /p:Version=${{ steps.version.outputs.VERSION }} - # ── MSIX packaging ──────────────────────────────────────────────────── - # TODO: Wire up Windows Application Packaging Project (wapproj) here - # once packaging/ scaffold is complete. For v1.0, zip the publish output - # as a stopgap so users can still download and run. - - - name: Package (stopgap zip) + # ── ZIP (GitHub Releases sideload) ──────────────────────────────────── + - name: Package zip shell: pwsh run: | Compress-Archive -Path publish/* -DestinationPath "WindowSeat-${{ steps.version.outputs.VERSION }}-win-x64.zip" + # ── MSIX (Microsoft Store submission) ───────────────────────────────── + - name: Patch manifest version + shell: pwsh + run: | + $v = "${{ steps.version.outputs.VERSION }}.0" + $path = "packaging/WindowSeat.Package/Package.appxmanifest" + (Get-Content $path -Raw) ` + -replace '(?<=]*Version=")[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+', $v | + Set-Content $path + Write-Host "Manifest version set to $v" + + - name: Build MSIX package + shell: pwsh + run: | + msbuild packaging/WindowSeat.Package/WindowSeat.Package.wapproj ` + /p:Configuration=Release ` + /p:Platform=x64 ` + /p:AppxPackageSigningEnabled=false ` + /p:AppxAutoIncrementPackageRevision=false ` + /p:GenerateTestArtifacts=false ` + "/p:AppxPackageDir=${{ github.workspace }}\artifacts\msix\\" + + - name: Upload MSIX (Store submission artifact) + uses: actions/upload-artifact@v4 + with: + name: WindowSeat-${{ steps.version.outputs.VERSION }}-store-package + path: artifacts/msix/*.msix + - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: diff --git a/.gitignore b/.gitignore index 0522e70..c3c951f 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,9 @@ coverage/ Thumbs.db Desktop.ini +# Git worktrees +.worktrees/ + # Secrets — never commit these *.pfx *.p12 diff --git a/README.md b/README.md index 1181d08..8db06bb 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ For advanced configuration see [docs/power-user-guide.md](docs/power-user-guide. ## Building from Source **Prerequisites** -- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) - Visual Studio 2022 (17.8+) with the **Windows application development** workload, or VS Code with the C# Dev Kit extension **Clone and build** diff --git a/WindowSeat.sln b/WindowSeat.sln index 9502d1a..cd0487c 100644 --- a/WindowSeat.sln +++ b/WindowSeat.sln @@ -10,6 +10,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowSeat.Settings", "src\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowSeat.Tests", "src\WindowSeat.Tests\WindowSeat.Tests.csproj", "{A1B2C3D4-0004-0004-0004-000000000004}" EndProject +Project("{C7167F0D-BC9F-4E6A-9F78-5F2B13F8E92E}") = "WindowSeat.Package", "packaging\WindowSeat.Package\WindowSeat.Package.wapproj", "{D1E2F3A4-0005-0005-0005-000000000005}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -32,5 +34,9 @@ Global {A1B2C3D4-0004-0004-0004-000000000004}.Debug|Any CPU.Build.0 = Debug|Any CPU {A1B2C3D4-0004-0004-0004-000000000004}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-0004-0004-0004-000000000004}.Release|Any CPU.Build.0 = Release|Any CPU + {D1E2F3A4-0005-0005-0005-000000000005}.Debug|Any CPU.ActiveCfg = Debug|x64 + {D1E2F3A4-0005-0005-0005-000000000005}.Debug|Any CPU.Build.0 = Debug|x64 + {D1E2F3A4-0005-0005-0005-000000000005}.Release|Any CPU.ActiveCfg = Release|x64 + {D1E2F3A4-0005-0005-0005-000000000005}.Release|Any CPU.Build.0 = Release|x64 EndGlobalSection EndGlobal diff --git a/docs/WindowSeat-Scope.md b/docs/WindowSeat-Scope.md index e1de308..10d9da4 100644 --- a/docs/WindowSeat-Scope.md +++ b/docs/WindowSeat-Scope.md @@ -4,7 +4,7 @@ **Last Updated:** 2026-03-30 **License:** MIT **Target Platform:** Windows 11 (primary), Windows 10 22H2+ (best effort) -**Stack:** .NET 8 LTS, C#, WPF, PowerShell (build/packaging tooling) +**Stack:** .NET 10, C#, WPF, PowerShell (build/packaging tooling) --- diff --git a/docs/contributing.md b/docs/contributing.md index 9903e70..98de1f3 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -6,7 +6,7 @@ Thank you for your interest in contributing. This document covers everything you ## Prerequisites -- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) - Visual Studio 2022 (17.8+) with the **Windows application development** workload _or_ VS Code with the [C# Dev Kit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit) extension - Git diff --git a/docs/superpowers/plans/2026-03-31-msix-installer.md b/docs/superpowers/plans/2026-03-31-msix-installer.md new file mode 100644 index 0000000..8f36377 --- /dev/null +++ b/docs/superpowers/plans/2026-03-31-msix-installer.md @@ -0,0 +1,601 @@ +# MSIX Installer Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a Windows Application Packaging Project (wapproj) that produces an unsigned MSIX for Microsoft Store submission, while keeping the existing GitHub Releases ZIP distribution unchanged. + +**Architecture:** A `.wapproj` in `packaging/WindowSeat.Package/` wraps `WindowSeat.App.csproj` and declares the package identity, entry point, and assets in `Package.appxmanifest`. CI builds the wapproj with MSBuild and uploads the MSIX as a workflow artifact for manual Partner Center submission. The existing `dotnet publish` → ZIP → GitHub Release flow is untouched. + +**Tech Stack:** MSBuild (wapproj toolset, pre-installed in VS 2022), `microsoft/setup-msbuild` GitHub Action, PowerShell 7 (manifest version patching, image generation), `actions/upload-artifact@v4` + +--- + +## File Map + +**Create:** +- `packaging/WindowSeat.Package/Images/Square44x44Logo.png` — 44×44 placeholder tile asset +- `packaging/WindowSeat.Package/Images/Square150x150Logo.png` — 150×150 placeholder tile asset +- `packaging/WindowSeat.Package/Images/Wide310x150Logo.png` — 310×150 placeholder tile asset +- `packaging/WindowSeat.Package/Images/StoreLogo.png` — 50×50 placeholder Store logo +- `packaging/WindowSeat.Package/Package.appxmanifest` — package identity, entry point, capabilities +- `packaging/WindowSeat.Package/WindowSeat.Package.wapproj` — Windows Application Packaging Project + +**Modify:** +- `WindowSeat.sln` — add wapproj as a solution project +- `.github/workflows/release.yml` — Setup MSBuild, patch manifest version, build wapproj, upload MSIX artifact +- `scripts/build.ps1` — extend `-Package` switch to also produce an MSIX via MSBuild +- `docs/user-guide.md` — update Installation and Uninstalling sections for the Store/ZIP model + +--- + +### Task 1: Create packaging directory and placeholder images + +> The MSIX manifest references four image assets. They must exist at the declared sizes or the wapproj build will fail. These are placeholders — replace with real artwork before the first Store submission. + +**Files:** +- Create: `packaging/WindowSeat.Package/Images/Square44x44Logo.png` +- Create: `packaging/WindowSeat.Package/Images/Square150x150Logo.png` +- Create: `packaging/WindowSeat.Package/Images/Wide310x150Logo.png` +- Create: `packaging/WindowSeat.Package/Images/StoreLogo.png` + +- [ ] **Step 1: Create the directory and generate placeholder PNGs** + +Run this PowerShell script from the repo root. It creates solid-colour PNGs at the exact sizes required by the manifest: + +```powershell +New-Item -ItemType Directory -Force -Path packaging/WindowSeat.Package/Images | Out-Null + +Add-Type -AssemblyName System.Drawing + +function New-PlaceholderPng { + param([string]$Path, [int]$Width, [int]$Height) + $bmp = New-Object System.Drawing.Bitmap($Width, $Height) + $g = [System.Drawing.Graphics]::FromImage($bmp) + $g.Clear([System.Drawing.Color]::FromArgb(255, 92, 45, 145)) # #5C2D91 purple + $g.Dispose() + $bmp.Save($Path, [System.Drawing.Imaging.ImageFormat]::Png) + $bmp.Dispose() +} + +New-PlaceholderPng -Path "packaging/WindowSeat.Package/Images/Square44x44Logo.png" -Width 44 -Height 44 +New-PlaceholderPng -Path "packaging/WindowSeat.Package/Images/Square150x150Logo.png" -Width 150 -Height 150 +New-PlaceholderPng -Path "packaging/WindowSeat.Package/Images/Wide310x150Logo.png" -Width 310 -Height 150 +New-PlaceholderPng -Path "packaging/WindowSeat.Package/Images/StoreLogo.png" -Width 50 -Height 50 +``` + +- [ ] **Step 2: Verify the files exist at the right sizes** + +```powershell +Get-ChildItem packaging/WindowSeat.Package/Images/ | Select-Object Name, Length +``` + +Expected: four `.png` files, each non-zero bytes. + +- [ ] **Step 3: Commit** + +```bash +git add packaging/WindowSeat.Package/Images/ +git commit -m "feat(packaging): add placeholder MSIX image assets" +``` + +--- + +### Task 2: Create Package.appxmanifest + +> The manifest declares the package identity (must match Partner Center exactly), the entry point, the target OS range, and the `runFullTrust` restricted capability (required whenever `EntryPoint="Windows.FullTrustApplication"` is used). + +**Files:** +- Create: `packaging/WindowSeat.Package/Package.appxmanifest` + +- [ ] **Step 1: Create the file** + +Create `packaging/WindowSeat.Package/Package.appxmanifest` with this exact content: + +```xml + + + + + + + WindowSeat + SteveBarnes + Images\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + +``` + +- [ ] **Step 2: Commit** + +```bash +git add packaging/WindowSeat.Package/Package.appxmanifest +git commit -m "feat(packaging): add MSIX package manifest" +``` + +--- + +### Task 3: Create WindowSeat.Package.wapproj + +> The wapproj is an MSBuild project that ties together the manifest, image assets, and the reference to `WindowSeat.App.csproj`. It uses the Desktop Bridge MSBuild targets installed with Visual Studio's "Windows application development" workload. +> +> **Prerequisite:** Visual Studio 2022 must be installed with the **"Windows application development"** workload (provides `Microsoft.DesktopBridge.props` and `Microsoft.DesktopBridge.targets`). On GitHub Actions `windows-latest`, these are present via the "Universal Windows Platform development" workload. + +**Files:** +- Create: `packaging/WindowSeat.Package/WindowSeat.Package.wapproj` + +- [ ] **Step 1: Create the file** + +Create `packaging/WindowSeat.Package/WindowSeat.Package.wapproj` with this exact content: + +```xml + + + + + + + Debug + x86 + {D1E2F3A4-0005-0005-0005-000000000005} + {C7167F0D-BC9F-4E6A-9F78-5F2B13F8E92E};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Package + false + false + Never + false + 10.0.22621.0 + 10.0.17763.0 + + + + pdbonly + false + bin\x64\Debug\AppPackages\ + + + + none + true + bin\x64\Release\AppPackages\ + + + + + Designer + + + + + + + + + + + + + true + + + + + + + +``` + +- [ ] **Step 2: Commit** + +```bash +git add packaging/WindowSeat.Package/WindowSeat.Package.wapproj +git commit -m "feat(packaging): add Windows Application Packaging Project" +``` + +--- + +### Task 4: Add wapproj to WindowSeat.sln + +> The solution file must reference the wapproj so Visual Studio can open it and so `dotnet build WindowSeat.sln` is aware of it. The wapproj uses a specific project type GUID (`{C7167F0D-...}`) that Visual Studio recognises as a packaging project. +> +> **Note:** `dotnet build` on the solution will skip the wapproj (it requires MSBuild, not the .NET CLI). This is expected and fine — the wapproj is only built explicitly via `msbuild`. + +**Files:** +- Modify: `WindowSeat.sln` + +- [ ] **Step 1: Add the project declaration** + +In `WindowSeat.sln`, insert this block immediately before the `Global` keyword (after the last `EndProject` line): + +``` +Project("{C7167F0D-BC9F-4E6A-9F78-5F2B13F8E92E}") = "WindowSeat.Package", "packaging\WindowSeat.Package\WindowSeat.Package.wapproj", "{D1E2F3A4-0005-0005-0005-000000000005}" +EndProject +``` + +- [ ] **Step 2: Add build configuration mappings** + +In `WindowSeat.sln`, inside `GlobalSection(ProjectConfigurationPlatforms) = postSolution`, add these four lines (before `EndGlobalSection`): + +``` + {D1E2F3A4-0005-0005-0005-000000000005}.Debug|Any CPU.ActiveCfg = Debug|x64 + {D1E2F3A4-0005-0005-0005-000000000005}.Debug|Any CPU.Build.0 = Debug|x64 + {D1E2F3A4-0005-0005-0005-000000000005}.Release|Any CPU.ActiveCfg = Release|x64 + {D1E2F3A4-0005-0005-0005-000000000005}.Release|Any CPU.Build.0 = Release|x64 +``` + +- [ ] **Step 3: Commit** + +```bash +git add WindowSeat.sln +git commit -m "feat(packaging): add WindowSeat.Package to solution" +``` + +--- + +### Task 5: Verify local wapproj build + +> Before touching CI, verify the wapproj builds on the local machine. This catches manifest errors, missing VS workloads, and path issues early. +> +> **Requires:** Visual Studio 2022 with the "Windows application development" workload installed. + +- [ ] **Step 1: Locate MSBuild** + +```powershell +$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" +$msbuild = & $vswhere -latest -requires Microsoft.Component.MSBuild ` + -find MSBuild\**\Bin\MSBuild.exe | Select-Object -First 1 +Write-Host "MSBuild: $msbuild" +``` + +Expected: a path like `C:\Program Files\Microsoft Visual Studio\2022\...\MSBuild\Current\Bin\MSBuild.exe` + +If `vswhere.exe` is not found or returns nothing, install the "Windows application development" workload in the Visual Studio Installer and retry. + +- [ ] **Step 2: Build the wapproj** + +```powershell +& $msbuild packaging/WindowSeat.Package/WindowSeat.Package.wapproj ` + /p:Configuration=Release ` + /p:Platform=x64 ` + /p:AppxPackageSigningEnabled=false ` + /p:AppxAutoIncrementPackageRevision=false ` + /p:GenerateTestArtifacts=false +``` + +Expected: `Build succeeded.` with no errors. Warnings about placeholder images are fine. + +- [ ] **Step 3: Verify the MSIX was produced** + +```powershell +Get-ChildItem packaging/WindowSeat.Package/bin/x64/Release/AppPackages/ -Recurse -Filter *.msix +``` + +Expected: one `.msix` file (e.g. `WindowSeat.Package_1.0.0.0_x64.msix`). + +- [ ] **Step 4: Inspect the MSIX contents (optional sanity check)** + +An MSIX is a zip file. Rename it to `.zip` and open it, or use: + +```powershell +$msix = Get-ChildItem packaging/WindowSeat.Package/bin/x64/Release/AppPackages/ -Recurse -Filter *.msix | Select-Object -First 1 +$zip = $msix.FullName -replace '\.msix$', '.zip' +Copy-Item $msix.FullName $zip +Expand-Archive $zip -DestinationPath "$($msix.Directory)\msix-contents" -Force +Get-ChildItem "$($msix.Directory)\msix-contents" | Select-Object Name +``` + +Expected to see: `WindowSeat.exe`, `AppxManifest.xml`, `AppxBlockMap.xml`, image files, and the app's DLLs. + +--- + +### Task 6: Update release.yml + +> Add three things to the CD pipeline: (1) `setup-msbuild` action so `msbuild.exe` is on PATH, (2) a PowerShell step that patches the manifest version from the git tag before building, (3) an MSBuild step to build the wapproj, and (4) an `upload-artifact` step to preserve the MSIX for Partner Center submission. The MSIX is NOT attached to the GitHub Release — it is a workflow artifact only. + +**Files:** +- Modify: `.github/workflows/release.yml` + +- [ ] **Step 1: Add `setup-msbuild` after the `Setup .NET 8` step** + +In `.github/workflows/release.yml`, after the `Setup .NET 8` step block, insert: + +```yaml + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v2 +``` + +- [ ] **Step 2: Add manifest version-patch step after "Publish app"** + +After the `Publish app` step block, insert: + +```yaml + # ── MSIX packaging ──────────────────────────────────────────────────── + - name: Patch manifest version + shell: pwsh + run: | + $v = "${{ steps.version.outputs.VERSION }}.0" + $path = "packaging/WindowSeat.Package/Package.appxmanifest" + (Get-Content $path -Raw) ` + -replace '(?<=]*Version=")[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+', $v | + Set-Content $path + Write-Host "Manifest version set to $v" +``` + +- [ ] **Step 3: Add MSBuild wapproj build step** + +Immediately after the manifest-patch step, insert: + +```yaml + - name: Build MSIX package + shell: pwsh + run: | + msbuild packaging/WindowSeat.Package/WindowSeat.Package.wapproj ` + /p:Configuration=Release ` + /p:Platform=x64 ` + /p:AppxPackageSigningEnabled=false ` + /p:AppxAutoIncrementPackageRevision=false ` + /p:GenerateTestArtifacts=false ` + "/p:AppxPackageDir=${{ github.workspace }}\artifacts\msix\\" +``` + +- [ ] **Step 4: Add upload-artifact step** + +Immediately after the `Build MSIX package` step, insert: + +```yaml + - name: Upload MSIX (Store submission artifact) + uses: actions/upload-artifact@v4 + with: + name: WindowSeat-${{ steps.version.outputs.VERSION }}-store-package + path: artifacts/msix/*.msix +``` + +- [ ] **Step 5: Update the TODO comment** + +Replace the existing TODO comment block in `release.yml`: + +```yaml + # ── MSIX packaging ──────────────────────────────────────────────────── + # TODO: Wire up Windows Application Packaging Project (wapproj) here + # once packaging/ scaffold is complete. For v1.0, zip the publish output + # as a stopgap so users can still download and run. +``` + +with just the section heading (the actual steps are now the ones added above): + +```yaml + # ── ZIP (GitHub Releases sideload) ──────────────────────────────────── +``` + +- [ ] **Step 6: Commit** + +```bash +git add .github/workflows/release.yml +git commit -m "feat(ci): build MSIX via wapproj and upload as Store submission artifact" +``` + +--- + +### Task 7: Update build.ps1 + +> Extend the existing `-Package` switch so that, after producing the ZIP, it also locates MSBuild and builds the wapproj to produce a local MSIX in `artifacts/`. If `vswhere.exe` or the VS workload is absent the script prints a clear warning and skips MSIX (not a hard failure — the ZIP is still produced). + +**Files:** +- Modify: `scripts/build.ps1` + +- [ ] **Step 1: Add `$ArtifactsDir` variable to the existing variable block** + +In `scripts/build.ps1`, after the `$TestProject` line, add: + +```powershell +$ArtifactsDir = Join-Path $RepoRoot 'artifacts' +$WaprojFile = Join-Path $RepoRoot 'packaging\WindowSeat.Package\WindowSeat.Package.wapproj' +``` + +- [ ] **Step 2: Replace the existing `-Package` block** + +Replace the existing `if ($Package)` block (lines 58–73) with: + +```powershell +# ── Publish ─────────────────────────────────────────────────────────────────── +if ($Package) { + Write-Step "Publishing" + if (Test-Path $PublishDir) { Remove-Item $PublishDir -Recurse -Force } + + dotnet publish (Join-Path $RepoRoot 'src/WindowSeat.App/WindowSeat.App.csproj') ` + --configuration $Configuration ` + --runtime win-x64 ` + --self-contained false ` + --output $PublishDir + if ($LASTEXITCODE -ne 0) { throw "Publish failed." } + + Write-Step "Creating ZIP" + $zipPath = Join-Path $RepoRoot "WindowSeat-local-win-x64.zip" + Compress-Archive -Path "$PublishDir/*" -DestinationPath $zipPath -Force + Write-Host "ZIP created: $zipPath" -ForegroundColor Green + + # ── MSIX (requires VS 2022 with Windows application development workload) ── + Write-Step "Building MSIX package" + $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + if (-not (Test-Path $vswhere)) { + Write-Warning "vswhere.exe not found — skipping MSIX. Install Visual Studio 2022 to enable local MSIX packaging." + } else { + $msbuild = & $vswhere -latest -requires Microsoft.Component.MSBuild ` + -find MSBuild\**\Bin\MSBuild.exe | Select-Object -First 1 + if (-not $msbuild) { + Write-Warning "MSBuild not found via vswhere — skipping MSIX." + } else { + $msixOut = Join-Path $ArtifactsDir 'msix' + if (Test-Path $msixOut) { Remove-Item $msixOut -Recurse -Force } + New-Item -ItemType Directory -Force -Path $msixOut | Out-Null + + & $msbuild $WaprojFile ` + /p:Configuration=$Configuration ` + /p:Platform=x64 ` + /p:AppxPackageSigningEnabled=false ` + /p:AppxAutoIncrementPackageRevision=false ` + /p:GenerateTestArtifacts=false ` + "/p:AppxPackageDir=$msixOut\\" + if ($LASTEXITCODE -ne 0) { throw "MSIX build failed." } + + $msix = Get-ChildItem $msixOut -Filter *.msix | Select-Object -First 1 + Write-Host "MSIX created: $($msix.FullName)" -ForegroundColor Green + } + } +} +``` + +- [ ] **Step 3: Verify the script runs cleanly without `-Package`** + +```powershell +.\scripts\build.ps1 -SkipTests +``` + +Expected: `Build complete.` with no errors. + +- [ ] **Step 4: Verify the script produces both ZIP and MSIX with `-Package`** + +```powershell +.\scripts\build.ps1 -Package -SkipTests +``` + +Expected: `ZIP created: ...` and `MSIX created: ...` (or a warning about missing VS workload if not installed). + +- [ ] **Step 5: Commit** + +```bash +git add scripts/build.ps1 +git commit -m "feat(build): add local MSIX packaging to build.ps1 -Package switch" +``` + +--- + +### Task 8: Update docs/user-guide.md + +> Replace the current GitHub-MSIX install instructions with the new Store-primary / ZIP-sideload model. The Uninstalling section already covers the Store path correctly; add the ZIP uninstall path. + +**Files:** +- Modify: `docs/user-guide.md` + +- [ ] **Step 1: Replace the Installation section** + +Replace the current `## Installation` section (lines 23–29) with: + +```markdown +## Installation + +### Microsoft Store (Recommended) + +1. Open the **Microsoft Store** on your PC +2. Search for **WindowSeat** or follow the link from the [GitHub Releases page](https://github.com/stmba/WindowSeat/releases) +3. Click **Get** (or **Install**) +4. WindowSeat starts automatically — look for the 🪑 icon in your system tray + +The Store version updates automatically and can be uninstalled cleanly from Settings → Apps. + +### ZIP Download (Technical Users / Sideload) + +If you prefer not to use the Store: + +1. Go to the [Releases page](https://github.com/stmba/WindowSeat/releases) on GitHub +2. Download the latest `WindowSeat-win-x64.zip` +3. Extract the ZIP to a folder of your choice (e.g. `C:\Program Files\WindowSeat\`) +4. Run `WindowSeat.exe` +5. Look for the 🪑 icon in your system tray + +> **Note:** The ZIP version has no installer. There is no entry in Add/Remove Programs. Auto-start at login works via the in-app Settings toggle as normal. +``` + +- [ ] **Step 2: Replace the Uninstalling section** + +Replace the current `## Uninstalling` section (lines 93–98) with: + +```markdown +## Uninstalling + +### Store version + +1. Open **Settings → Apps → Installed apps** +2. Search for **WindowSeat** +3. Click the three-dot menu and choose **Uninstall** + +### ZIP version + +1. Right-click the 🪑 tray icon and choose **Exit** +2. Delete the folder you extracted WindowSeat into +3. If you enabled auto-start, open **Task Manager → Startup apps** and remove the WindowSeat entry (or toggle it off in Settings before exiting) + +In both cases, WindowSeat's settings file at `%AppData%\WindowSeat\settings.json` is not removed automatically. Delete that folder manually if you want a complete clean removal. +``` + +- [ ] **Step 3: Commit** + +```bash +git add docs/user-guide.md +git commit -m "docs: update installation and uninstall instructions for Store/ZIP model" +``` + +--- + +## Self-Review + +**Spec coverage check:** + +| Spec requirement | Task | +|---|---| +| wapproj in `packaging/WindowSeat.Package/` | Task 3 | +| Package identity: `SteveBarnes.WindowSeat` / `CN=4E8B8DF2-...` | Task 2 | +| Entry point: `Windows.FullTrustApplication` | Task 2 | +| `runFullTrust` capability | Task 2 | +| `TargetDeviceFamily` min `10.0.17763.0` | Task 2 | +| Placeholder image assets at correct sizes | Task 1 | +| wapproj added to `WindowSeat.sln` | Task 4 | +| CI: `setup-msbuild` action | Task 6 | +| CI: manifest version patched from git tag | Task 6 | +| CI: wapproj built with `AppxPackageSigningEnabled=false` | Task 6 | +| CI: MSIX uploaded as workflow artifact (not GitHub Release attachment) | Task 6 | +| CI: ZIP flow unchanged | Task 6 (no-touch) | +| `build.ps1 -Package` builds MSIX locally | Task 7 | +| `build.ps1` gracefully skips MSIX if VS not installed | Task 7 | +| `docs/user-guide.md` reflects Store/ZIP model | Task 8 | +| Future signed-release path documented | Spec only (no code needed) | + +All spec requirements covered. No placeholders in any task. diff --git a/docs/superpowers/specs/2026-03-31-msix-installer-design.md b/docs/superpowers/specs/2026-03-31-msix-installer-design.md new file mode 100644 index 0000000..48f23c6 --- /dev/null +++ b/docs/superpowers/specs/2026-03-31-msix-installer-design.md @@ -0,0 +1,158 @@ +# MSIX Installer Design + +**Date:** 2026-03-31 +**Status:** Approved +**Distribution model:** Microsoft Store (primary) + GitHub Releases ZIP sideload (secondary) + +--- + +## Overview + +Add an MSIX installer to WindowSeat using a Windows Application Packaging Project (wapproj). The MSIX is built in CI and uploaded to the Microsoft Partner Center for Store distribution. GitHub Releases continues to distribute a plain ZIP for technical users who want to run the app without installing from the Store. + +--- + +## Package Identity + +| Field | Value | +|---|---| +| Name | `SteveBarnes.WindowSeat` | +| Publisher | `CN=4E8B8DF2-E078-41CF-871B-1CEF8CC0E1DC` | +| Version | Derived from git tag at build time (e.g. `v1.0.0` → `1.0.0.0`) | + +The publisher CN matches the Partner Center app registration. Microsoft signs the package during Store ingestion — no certificate management is required in this project. + +--- + +## Project Structure + +New files added under `packaging/WindowSeat.Package/`: + +``` +packaging/ +└── WindowSeat.Package/ + ├── WindowSeat.Package.wapproj ← Windows Application Packaging Project + ├── Package.appxmanifest ← Package identity, entry point, capabilities + └── Images/ + ├── Square44x44Logo.png ← Required (tray/taskbar size) + ├── Square150x150Logo.png ← Required (Start menu tile) + ├── Wide310x150Logo.png ← Required (wide Start tile) + └── StoreLogo.png ← Required (Partner Center listing) +``` + +`WindowSeat.sln` is updated to include `WindowSeat.Package.wapproj` as a solution project. The wapproj holds no code — it is a thin packaging wrapper that references `WindowSeat.App.csproj` as its entry point. + +--- + +## Package Manifest (Package.appxmanifest) + +### Identity and Properties + +```xml + +``` + +Version is injected at build time via MSBuild property (`/p:PackageVersion`). + +### Entry Point + +The app is declared as a full-trust desktop application via `EntryPoint="Windows.FullTrustApplication"` on the `Application` element: + +```xml + + + +``` + +This is the correct declaration for a wapproj-packaged WPF app. (`windows.fullTrustProcess` is a different extension used by UWP apps to spawn Win32 companion processes — it is not needed here.) + +The manifest also declares `TargetDeviceFamily` targeting Windows Desktop with a minimum version of `10.0.17763.0` (Windows 10 1809), consistent with the project's stated minimum. + +### Capabilities + +None declared. The app: +- Reads/writes `%AppData%\WindowSeat\` (implicit for packaged apps via AppData redirection) +- Writes a startup registry key under `HKCU` (no capability required) +- Calls Win32 hooks (`SetWinEventHook`) — no capability required +- Makes one outbound HTTPS call to the GitHub API (internet client capability is implicit for packaged desktop apps on Windows 10+) + +### Required Images + +Placeholder PNG assets are committed to `packaging/WindowSeat.Package/Images/`. These should be replaced with production-quality artwork before the first Store submission. + +| Asset | Size | +|---|---| +| Square44x44Logo.png | 44×44 | +| Square150x150Logo.png | 150×150 | +| Wide310x150Logo.png | 310×150 | +| StoreLogo.png | 50×50 | + +--- + +## CI/CD Pipeline Changes + +### release.yml + +Two additions alongside the existing flow: + +1. **Setup MSBuild** — adds `msbuild.exe` to PATH using the `microsoft/setup-msbuild` action. VS build tools including the Windows Application Packaging toolset are pre-installed on `windows-latest`. + +2. **Build wapproj** — MSBuild builds `packaging/WindowSeat.Package/WindowSeat.Package.wapproj` in Release/x64 with version injected: + ``` + msbuild packaging/WindowSeat.Package/WindowSeat.Package.wapproj + /p:Configuration=Release + /p:Platform=x64 + /p:PackageVersion={version}.0 + /p:AppxPackageSigningEnabled=false + ``` + `AppxPackageSigningEnabled=false` produces an unsigned MSIX suitable for Partner Center submission (Microsoft signs during ingestion). + +3. **Upload MSIX as workflow artifact** — the MSIX is uploaded via `actions/upload-artifact`, not attached to the public GitHub Release. This keeps the release page uncluttered for end users. The maintainer downloads it from the Actions run when submitting to Partner Center. + +### Existing ZIP flow (unchanged) + +`dotnet publish` → `Compress-Archive` → attached to GitHub Release. No changes. + +--- + +## Local Build Script (scripts/build.ps1) + +A `-Package` switch is added. When passed: + +1. Locates `msbuild.exe` via `vswhere.exe` (`${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe`) +2. Builds the wapproj (same MSBuild arguments as CI) +3. Copies the resulting MSIX to `artifacts/` + +Without `-Package`, existing behaviour is unchanged. + +--- + +## Documentation Changes (docs/user-guide.md) + +The installation section is updated to reflect the new distribution model: + +- **Recommended:** Install from the Microsoft Store (one click, automatic updates, clean uninstall via Settings → Apps → Installed apps) +- **Sideload (technical users):** Download the ZIP from GitHub Releases, extract, and run `WindowSeat.exe` directly. No Add/Remove Programs entry; uninstall by deleting the folder and optionally removing `%AppData%\WindowSeat\`. + +--- + +## Out of Scope + +- Code signing for the GitHub ZIP or MSIX sideload (deferred — add a trusted cert to the signing step in `release.yml` when available) +- MSIX bundle (`.msixbundle`) for multi-architecture support — single x64 package is sufficient for v1.0 +- Auto-update via Store — the existing GitHub API update check remains in place; Store handles its own updates independently + +--- + +## Future: Adding Signed GitHub Releases (Option B) + +When a trusted code-signing certificate becomes available (e.g. SignPath.io OSS program): + +1. Store the PFX as a base64-encoded GitHub Actions secret (`SIGNING_CERT_PFX`) and its password as `SIGNING_CERT_PASSWORD` +2. Add a `signtool.exe` step after the wapproj build in `release.yml` +3. Attach the signed MSIX to the GitHub Release alongside the ZIP + +No structural changes to the wapproj, manifest, or local build script are required. diff --git a/docs/user-guide.md b/docs/user-guide.md index dbe8966..5e9eaad 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -20,13 +20,26 @@ When you open an application on a multi-monitor Windows setup, Windows often ign ## Installation +### Microsoft Store (Recommended) + +1. Open the **Microsoft Store** on your PC +2. Search for **WindowSeat** or follow the link from the [GitHub Releases page](https://github.com/stmba/WindowSeat/releases) +3. Click **Get** (or **Install**) +4. WindowSeat starts automatically — look for the 🪑 icon in your system tray + +The Store version updates automatically and can be uninstalled cleanly from Settings → Apps. + +### ZIP Download (Technical Users / Sideload) + +If you prefer not to use the Store: + 1. Go to the [Releases page](https://github.com/stmba/WindowSeat/releases) on GitHub -2. Download the latest `WindowSeat.msix` file -3. Double-click the downloaded file -4. If Windows SmartScreen appears, click **More info** then **Run anyway** - _(This warning appears because WindowSeat is not yet signed with a paid certificate. The source code is fully open for review at github.com/stmba/WindowSeat.)_ -5. Follow the installer prompts and click **Install** -6. WindowSeat starts automatically — look for the 🪑 icon in your system tray +2. Download the latest `WindowSeat-win-x64.zip` +3. Extract the ZIP to a folder of your choice (e.g. `C:\Program Files\WindowSeat\`) +4. Run `WindowSeat.exe` +5. Look for the 🪑 icon in your system tray + +> **Note:** The ZIP version has no installer. There is no entry in Add/Remove Programs. Auto-start at login works via the in-app Settings toggle as normal. --- @@ -91,11 +104,19 @@ Click **Save** to apply your changes. Click **Cancel** to discard them. ## Uninstalling +### Store version + 1. Open **Settings → Apps → Installed apps** 2. Search for **WindowSeat** 3. Click the three-dot menu and choose **Uninstall** -WindowSeat's settings file at `%AppData%\WindowSeat\settings.json` is not removed by the uninstaller. You can delete this folder manually if you want a complete clean removal. +### ZIP version + +1. Right-click the 🪑 tray icon and choose **Exit** +2. Delete the folder you extracted WindowSeat into +3. If you enabled auto-start, open **Task Manager → Startup apps** and remove the WindowSeat entry (or toggle it off in Settings before exiting) + +In both cases, WindowSeat's settings file at `%AppData%\WindowSeat\settings.json` is not removed automatically. Delete that folder manually if you want a complete clean removal. --- diff --git a/packaging/WindowSeat.Package/Images/Square150x150Logo.png b/packaging/WindowSeat.Package/Images/Square150x150Logo.png new file mode 100644 index 0000000..f6df41e Binary files /dev/null and b/packaging/WindowSeat.Package/Images/Square150x150Logo.png differ diff --git a/packaging/WindowSeat.Package/Images/Square44x44Logo.png b/packaging/WindowSeat.Package/Images/Square44x44Logo.png new file mode 100644 index 0000000..230aad3 Binary files /dev/null and b/packaging/WindowSeat.Package/Images/Square44x44Logo.png differ diff --git a/packaging/WindowSeat.Package/Images/StoreLogo.png b/packaging/WindowSeat.Package/Images/StoreLogo.png new file mode 100644 index 0000000..e8b5c0b Binary files /dev/null and b/packaging/WindowSeat.Package/Images/StoreLogo.png differ diff --git a/packaging/WindowSeat.Package/Images/Wide310x150Logo.png b/packaging/WindowSeat.Package/Images/Wide310x150Logo.png new file mode 100644 index 0000000..82e635f Binary files /dev/null and b/packaging/WindowSeat.Package/Images/Wide310x150Logo.png differ diff --git a/packaging/WindowSeat.Package/Package.appxmanifest b/packaging/WindowSeat.Package/Package.appxmanifest new file mode 100644 index 0000000..60b496a --- /dev/null +++ b/packaging/WindowSeat.Package/Package.appxmanifest @@ -0,0 +1,48 @@ + + + + + + + WindowSeat + SteveBarnes + Images\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packaging/WindowSeat.Package/WindowSeat.Package.wapproj b/packaging/WindowSeat.Package/WindowSeat.Package.wapproj new file mode 100644 index 0000000..8fee242 --- /dev/null +++ b/packaging/WindowSeat.Package/WindowSeat.Package.wapproj @@ -0,0 +1,56 @@ + + + + + + + Debug + x64 + {D1E2F3A4-0005-0005-0005-000000000005} + {C7167F0D-BC9F-4E6A-9F78-5F2B13F8E92E};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Package + false + false + Never + false + 10.0.22621.0 + 10.0.17763.0 + + + + pdbonly + false + bin\x64\Debug\AppPackages\ + + + + none + true + bin\x64\Release\AppPackages\ + + + + + Designer + + + + + + + + + + + + + true + + + + + + + diff --git a/scripts/build.ps1 b/scripts/build.ps1 index 0fc32ba..563aaab 100644 --- a/scripts/build.ps1 +++ b/scripts/build.ps1 @@ -32,6 +32,8 @@ $RepoRoot = Split-Path $PSScriptRoot -Parent $SolutionFile = Join-Path $RepoRoot 'WindowSeat.sln' $PublishDir = Join-Path $RepoRoot 'publish' $TestProject = Join-Path $RepoRoot 'src/WindowSeat.Tests/WindowSeat.Tests.csproj' +$ArtifactsDir = Join-Path $RepoRoot 'artifacts' +$WaprojFile = Join-Path $RepoRoot 'packaging\WindowSeat.Package\WindowSeat.Package.wapproj' function Write-Step([string]$message) { Write-Host "`n==> $message" -ForegroundColor Cyan @@ -66,10 +68,39 @@ if ($Package) { --output $PublishDir if ($LASTEXITCODE -ne 0) { throw "Publish failed." } - Write-Step "Creating zip" + Write-Step "Creating ZIP" $zipPath = Join-Path $RepoRoot "WindowSeat-local-win-x64.zip" Compress-Archive -Path "$PublishDir/*" -DestinationPath $zipPath -Force - Write-Host "Package created: $zipPath" -ForegroundColor Green + Write-Host "ZIP created: $zipPath" -ForegroundColor Green + + # ── MSIX (requires VS 2022 with Windows application development workload) ── + Write-Step "Building MSIX package" + $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + if (-not (Test-Path $vswhere)) { + Write-Warning "vswhere.exe not found — skipping MSIX. Install Visual Studio 2022 to enable local MSIX packaging." + } else { + $msbuild = & $vswhere -latest -requires Microsoft.Component.MSBuild ` + -find MSBuild\**\Bin\MSBuild.exe | Select-Object -First 1 + if (-not $msbuild) { + Write-Warning "MSBuild not found via vswhere — skipping MSIX." + } else { + $msixOut = Join-Path $ArtifactsDir 'msix' + if (Test-Path $msixOut) { Remove-Item $msixOut -Recurse -Force } + New-Item -ItemType Directory -Force -Path $msixOut | Out-Null + + & $msbuild $WaprojFile ` + /p:Configuration=$Configuration ` + /p:Platform=x64 ` + /p:AppxPackageSigningEnabled=false ` + /p:AppxAutoIncrementPackageRevision=false ` + /p:GenerateTestArtifacts=false ` + "/p:AppxPackageDir=$msixOut\\" + if ($LASTEXITCODE -ne 0) { throw "MSIX build failed." } + + $msix = Get-ChildItem $msixOut -Filter *.msix | Select-Object -First 1 + Write-Host "MSIX created: $($msix.FullName)" -ForegroundColor Green + } + } } Write-Host "`nBuild complete." -ForegroundColor Green diff --git a/src/WindowSeat.App/App.xaml.cs b/src/WindowSeat.App/App.xaml.cs index da5560b..1f8093c 100644 --- a/src/WindowSeat.App/App.xaml.cs +++ b/src/WindowSeat.App/App.xaml.cs @@ -31,7 +31,7 @@ protected override async void OnStartup(StartupEventArgs e) ? new CursorMonitorDetector() : new TaskbarMonitorDetector(); - _mover = new WindowMover(_settingsService, detector); + _mover = new WindowMover(detector); _listener = new WindowEventListener(); _listener.WindowShown += OnWindowShown; _listener.Start(); diff --git a/src/WindowSeat.App/Assets/windowseat.ico b/src/WindowSeat.App/Assets/windowseat.ico new file mode 100644 index 0000000..f4a7273 Binary files /dev/null and b/src/WindowSeat.App/Assets/windowseat.ico differ diff --git a/src/WindowSeat.App/Views/SettingsWindow.xaml b/src/WindowSeat.App/Views/SettingsWindow.xaml index 70ac87c..eb00414 100644 --- a/src/WindowSeat.App/Views/SettingsWindow.xaml +++ b/src/WindowSeat.App/Views/SettingsWindow.xaml @@ -129,7 +129,7 @@ - net8.0-windows + net10.0-windows true enable enable diff --git a/src/WindowSeat.Core/WindowSeat.Core.csproj b/src/WindowSeat.Core/WindowSeat.Core.csproj index 7bd7350..0b6c815 100644 --- a/src/WindowSeat.Core/WindowSeat.Core.csproj +++ b/src/WindowSeat.Core/WindowSeat.Core.csproj @@ -1,6 +1,6 @@ - net8.0-windows + net10.0-windows enable enable latest diff --git a/src/WindowSeat.Settings/WindowSeat.Settings.csproj b/src/WindowSeat.Settings/WindowSeat.Settings.csproj index f023c72..b89e89d 100644 --- a/src/WindowSeat.Settings/WindowSeat.Settings.csproj +++ b/src/WindowSeat.Settings/WindowSeat.Settings.csproj @@ -1,13 +1,10 @@ - net8.0-windows + net10.0-windows enable enable latest WindowSeat.Settings WindowSeat.Settings - - - diff --git a/src/WindowSeat.Tests/WindowSeat.Tests.csproj b/src/WindowSeat.Tests/WindowSeat.Tests.csproj index 0385cdd..5a539bc 100644 --- a/src/WindowSeat.Tests/WindowSeat.Tests.csproj +++ b/src/WindowSeat.Tests/WindowSeat.Tests.csproj @@ -1,6 +1,6 @@ - net8.0-windows + net10.0-windows enable enable latest