Skip to content

Commit cefce39

Browse files
ranjeshjCopilotshanselman
authored
Redesign local gateway setup as out-of-process SetupEngine (#529)
* feat: add standalone SetupEngine with transactional pipeline New headless setup engine that installs WSL, configures OpenClaw gateway, pairs operator/node connections, and verifies end-to-end connectivity. - Transactional pipeline with retry, rollback, and crash-recovery journal - Structured JSONL logging with secret redaction - v2 signature fix for local gateways in GatewayConnectionManager - All 15 steps working E2E in headless mode Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: gateway bind lan + keepalive step for reliable WSL2 connectivity - Configure gateway with bind=lan (0.0.0.0) instead of loopback to avoid unreliable WSL2 localhost port forwarding - Add StartKeepaliveStep (step 16) to keep distro alive after setup completes, ensuring tray connects instantly on launch - Add default-config.json and design doc Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(setup-engine): config-driven capabilities, settings, and WSL/gateway config - Expand SetupConfig with nested WslConfig, GatewayConfig, CapabilitiesConfig, TraySettingsConfig, and PairingConfig — zero hardcoded values - Register node capabilities (stub INodeCapability) before ConnectAsync so gateway stores caps/commands from hello message - Write settings.json after node pairing (EnableNodeMode=true + cap toggles) using merge logic that preserves existing user settings - Make WSL wsl.conf generation config-driven (user, systemd, interop, etc.) - Make gateway config-driven (bind mode, auth, health timeout, extra config) - Write keepalive marker file to prevent tray duplicate keepalive - Add fully commented default-config.json with all configurable properties Verified: clean build (0 errors/0 warnings), full E2E run in 118s, tray auto-connects with capabilities registered. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(setup-engine): drain pending device/node approvals at end of setup Adds DrainPendingApprovalsAsync to VerifyEndToEndStep that iteratively approves any remaining pending device or node pairing requests. This ensures the tray launches with zero 'Pairing approval pending' badges. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore(setup-engine): remove remote gateway mode (local-wsl only) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add SetupEngine WinUI app with fluent onboarding flow Standalone unpackaged WinUI app (no FunctionalUI dependency) with: - Welcome page with lobster icon, info card, V2 text strings - Capabilities page with 2-column grid, icons, descriptions, toggles - Progress page with step groups, badges (spinner/check/error), log viewer - Complete page with success/error state, launch tray button Features: - Mica backdrop + extended title bar - DPI-aware window sizing (720x700 logical) - UAC manifest (asInvoker) to avoid elevation prompt - --headless bypass for automation - Config-driven defaults from SetupConfig Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Polish setup engine UI to match V2 onboarding flow - Welcome: flex layout, full V2 text, 'Install new WSL Gateway' button, confirmation dialog, 'Advanced setup' link (opens tray connection page) - Title bar: lobster icon + 'OpenClaw Setup' text, 36px height - Permissions page: 5 rows (notifications, camera, mic, location, screen), live status checks, 'Open Settings' buttons, 'Refresh status' - Complete page: party popper, amber Node Mode banner, startup toggle, 'Finish' button with tray launch + optional registry startup entry - Window height increased to 820px for better step row spacing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Require config file — no hardcoded defaults - Remove all hardcoded defaults from SetupConfig (DistroName, GatewayPort, BaseDistro, etc.) - Both UI and headless exe now require a config file to run - UI auto-loads bundled default-config.json from AppContext.BaseDirectory - Headless Program.cs exits with error if no config found - Added Content Include in csproj to bundle default-config.json with exe Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix cleanup, add error UX, launch tray via deep link - CleanupStaleDistroStep: wsl --shutdown + delete orphaned VHD directory - CompletePage: show error message + 'View full log' link on failure - CompletePage: kill old tray, launch via openclaw://chat protocol - Update SETUP_ENGINE_REDESIGN.md to reflect current implementation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add setup engine wizard flow * Polish setup wizard UI and WSL setup resilience - add interactive gateway wizard rendering for SetupEngine UI - make wizard messages render links and device codes inline - refine progress/log layout and setup failure visuals - fix wizard retry/skip behavior and credential precedence - harden WSL cleanup, base distro reuse, and missing WSL handling Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Replace old onboarding/setup with out-of-process SetupEngine.UI - Add RollbackAsync to 6 SetupEngine steps for --uninstall support - Add UninstallAsync to SetupPipeline (reverse rollback execution) - Add --uninstall, --confirm-destructive, --json-output CLI flags - Replace ShowOnboardingAsync with out-of-process SetupEngine.UI launch - Rewrite CliUninstallHandler and SettingsPage uninstall to use SetupEngine - Delete LocalGatewaySetup (4000+ lines), Onboarding, OnboardingV2 projects - Extract GatewayConnectorInterfaces and WslCommandRunner from deleted code - Fix StartupSetupState to scan per-gateway dirs for device tokens - Simplify WSL keepalive to direct wsl process spawn - Add SetupEngine to build.ps1 with post-build copy to WinUI output - Set production defaults: DistroName=OpenClawGateway, GatewayPort=18789 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add E2E first-time setup CI test Create tests/OpenClaw.E2ETests/ project with end-to-end tests that exercise the full setup pipeline headless via SetupEngine CLI, spawn the tray app, and verify operator+node connectivity through MCP app.status/app.nodes calls. - E2ESetupFixture: runs Program.Main() headless, patches settings for MCP, spawns tray process, polls connection status, cleans up via uninstall - SetupAndConnectTests: verifies connected state and node capabilities - McpClient: JSON-RPC client for MCP HTTP server verification - CI workflow: parallel e2e job with test artifact upload for debugging - SetupEngine.csproj: add RuntimeIdentifiers for RID-specific test builds Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: setup engine reliability fixes and unit tests Hanselman adversarial review (Opus + Codex) identified 18 issues. Fixed 12 reliability bugs across the setup engine: HIGH consensus (both models): - RetryExecutor: wrap action() in try/catch to prevent pipeline crashes - CommandRunner: catch Win32Exception on process.Start() - ConfigureGatewayStep: shell-escape ExtraConfig values Verified LOW consensus: - TryResetReloadModeAsync: use CancellationToken.None in finally - TransactionJournal: catch IOException on writes - CleanupStaleGatewayStep: delete setup-state.json from both AppData and LocalAppData - AutoApprove: fall back to BootstrapToken when SharedGatewayToken is null - TrayArtifactCleanup: protect DeleteFileIfExists with try/catch - StartKeepaliveStep: remove unused stdout/stderr redirect - PairOperatorStep: unsubscribe DeviceTokenReceived handler - Program: run TrayArtifactCleanup on Cancelled outcome - CleanupStaleDistroStep: retry VHD directory deletion with backoff Added 66 unit tests in new OpenClaw.SetupEngine.Tests project: - RetryExecutorTests (11), SetupPipelineTests (14), TransactionJournalTests (9), SetupLoggerTests (7), SetupConfigTests (18), SetupContextTests (7) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: replace OnboardingV2 tests with SetupEngine tests, fix E2E restore - Replace OnboardingV2.Tests build/run steps with SetupEngine.Tests (66 unit tests) - Remove empty OnClawTray.OnboardingV2.Tests project (superseded by SetupEngine) - Drop --no-restore from E2E build step to fix RID-mismatch NETSDK1004 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: fix SetupEngine.Tests restore and rename e2e job - Add -r win-x64 to SetupEngine.Tests build (allows implicit restore for RID-specific deps) - Add -r win-x64 to SetupEngine.Tests run step - Rename e2e job to e2etests for clarity Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address ClawSweeper P1 findings — gateway cleanup, URL validation, release packaging - CleanupStaleGatewayStep: preserve SSH-tunneled and non-local gateway records instead of deleting by URL match alone - InstallCliStep: validate HTTPS scheme, shell-quote URL, add --proto '=https' --tlsv1.2 to curl - ci.yml: publish SetupEngine.UI into release package - Add 8 unit tests covering gateway preservation and URL validation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Harden setup engine: WSL lockdown, secure defaults, token handling, diagnostics - Default gateway bind from 'lan' (0.0.0.0) to 'loopback' (127.0.0.1) - Add ValidateWslLockdownStep: verify user, dirs, ownership after configure - Replace --token argv with OPENCLAW_GATEWAY_TOKEN env var (9 call sites) - Add ExistingConfigDetector for dynamic replacement dialog on WelcomePage - Remove unimplemented OperatorScopes/NodeScopes/CliScopes from PairingConfig - Add port conflict detection (ss -tlnp) and improved failure diagnostics - Add RedactTokens helper for log sanitization - Default SkipPermissions to false, add fallback tray exe path - Fix docs drift: step count, default claims - Add UI step group mappings for validate-wsl-lockdown and run-wizard - 279 new lines of unit tests (lockdown, bind validation, token redaction, etc.) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Harden setup engine reliability Add setup run locking, append-only recovery journals, atomic persistence, bounded command output and rollback handling, UI cancellation/error guards, wizard loop bounds, isolated local data support, and expanded token redaction. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix setup engine WSL isolation Ensure setup runs privileged WSL configuration as root so imported base distros with non-root defaults still configure correctly. Align local AppData override handling across SetupEngine, tray setup detection, keepalive, and e2e isolation, and accept numeric setup-state phases written by the new engine. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Stabilize setup engine CI tests Serialize SetupEngine tests that mutate process-wide environment variables and make the E2E fixture wait until app.nodes reports an online node with capabilities before allowing tests to proceed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Harden setup engine approval and rollback flows Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Bring setup window to foreground Focus SetupEngine.UI when launched from the tray and briefly make the setup window topmost so user-initiated setup actions are visible. Reuse the tray icon for the setup app so both companion surfaces share branding. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use isolated identity path for setup detection Prefer OPENCLAW_TRAY_DATA_DIR for tray identity checks in isolated runs so startup setup detection sees the per-gateway device tokens written by SetupEngine and does not relaunch setup after a successful install. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix empty cron and event stream states Mark cron loading complete when the gateway returns an empty jobs list, and place the Event Stream empty state in the main content row so the page does not look blank when no agent events have arrived yet. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix event stream page localization crash Remove an invalid x:Uid from the Event Stream clear-button TextBlock. The matching resource includes a Content property, which TextBlock does not support, causing AgentEventsPage to throw during XAML load and making navigation appear to do nothing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Harden setup engine gateway configuration Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix connection dashboard token link Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Improve setup wizard option hints Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Polish setup UI assets and welcome animation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address setup hardening feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Harden setup approval drain Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Harden setup wizard validation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Polish setup progress indicators Use Fluent icon glyphs for completed and failed setup steps, add a subtle pending indicator, and give the active progress ring enough space to render cleanly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix tray UI test runtime bootstrap Align the UI test WinAppSDK bootstrap/runtime install with the Microsoft.WindowsAppSDK package version and add fixture startup diagnostics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix WSL keepalive startup Use the active gateway registry record when deciding whether tray startup should keep the local WSL gateway alive, and add unit and e2e coverage for the keepalive process. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address remaining setup feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Scott Hanselman <scottha@microsoft.com>
1 parent ef6ac8a commit cefce39

144 files changed

Lines changed: 12602 additions & 19110 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 85 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ jobs:
8585
dotnet build tests/OpenClaw.Tray.IntegrationTests -c Debug -r win-x64 --no-restore
8686
dotnet build tests/OpenClaw.Tray.UITests -c Debug -r win-x64 --no-restore
8787
dotnet build tests/OpenClawTray.FunctionalUI.Tests -c Debug -r win-x64 --no-restore
88-
dotnet build tests/OpenClawTray.OnboardingV2.Tests -c Debug -r win-x64 --no-restore
88+
dotnet build tests/OpenClaw.SetupEngine.Tests -c Debug -r win-x64
8989
9090
- name: Run Shared Tests
9191
env:
@@ -158,14 +158,14 @@ jobs:
158158
--logger trx;LogFileName=OpenClaw.Tray.IntegrationTests.trx"
159159
160160
# UI tests need a real visual tree AND a system-registered WindowsAppRuntime
161-
# framework MSIX — the test fixture calls Bootstrap.Initialize(1.8, stable),
161+
# framework MSIX — the test fixture calls Bootstrap.Initialize(2.0, stable),
162162
# which looks up the framework package by identity. The hosted windows-2025
163163
# runner image doesn't preinstall it, so we install it explicitly here.
164-
# Version pinned to match Microsoft.WindowsAppSDK 1.8.260101001 in the csprojs.
165-
- name: Install WindowsAppRuntime 1.8
164+
# Version pinned to match Microsoft.WindowsAppSDK 2.0.1 in the csprojs.
165+
- name: Install WindowsAppRuntime 2.0.1
166166
shell: pwsh
167167
run: |
168-
$url = "https://aka.ms/windowsappsdk/1.8/1.8.260101001/windowsappruntimeinstall-x64.exe"
168+
$url = "https://aka.ms/windowsappsdk/2.0/2.0.1/windowsappruntimeinstall-x64.exe"
169169
$exe = "$env:RUNNER_TEMP\WindowsAppRuntimeInstall.exe"
170170
Invoke-WebRequest -Uri $url -OutFile $exe
171171
& $exe --quiet
@@ -184,18 +184,18 @@ jobs:
184184
--results-directory TestResults\FunctionalUI
185185
--logger trx;LogFileName=OpenClawTray.FunctionalUI.Tests.trx"
186186
187-
- name: Run Onboarding V2 Tests
187+
- name: Run SetupEngine Tests
188188
run: >
189189
dotnet-coverage collect
190-
--output TestResults\OnboardingV2\coverage.cobertura.xml
190+
--output TestResults\SetupEngine\coverage.cobertura.xml
191191
--output-format cobertura
192-
"dotnet test tests/OpenClawTray.OnboardingV2.Tests
192+
"dotnet test tests/OpenClaw.SetupEngine.Tests
193193
--no-build
194194
-c Debug
195195
-r win-x64
196196
--verbosity normal
197-
--results-directory TestResults\OnboardingV2
198-
--logger trx;LogFileName=OpenClawTray.OnboardingV2.Tests.trx"
197+
--results-directory TestResults\SetupEngine
198+
--logger trx;LogFileName=OpenClaw.SetupEngine.Tests.trx"
199199
200200
- name: Run Tray UI Tests
201201
run: >
@@ -222,8 +222,72 @@ jobs:
222222
semVer: ${{ steps.gitversion.outputs.semVer }}
223223
majorMinorPatch: ${{ steps.gitversion.outputs.majorMinorPatch }}
224224

225+
e2etests:
226+
needs: repo-hygiene
227+
if: ${{ !cancelled() }}
228+
runs-on: windows-latest
229+
timeout-minutes: 15
230+
steps:
231+
- name: Fail if repo hygiene failed
232+
if: ${{ needs.repo-hygiene.result != 'success' }}
233+
shell: pwsh
234+
run: |
235+
Write-Error "repo-hygiene failed; see the repo-hygiene job output."
236+
exit 1
237+
238+
- uses: actions/checkout@v6
239+
with:
240+
fetch-depth: 0
241+
242+
- name: Setup .NET 10
243+
uses: actions/setup-dotnet@v5
244+
with:
245+
dotnet-version: 10.0.x
246+
247+
- name: Cache NuGet packages
248+
uses: actions/cache@v5
249+
with:
250+
path: ~/.nuget/packages
251+
key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj', '**/Directory.Packages.props') }}
252+
restore-keys: nuget-${{ runner.os }}-
253+
254+
- name: Restore dependencies
255+
run: dotnet restore
256+
257+
- name: Build Shared Library
258+
run: dotnet build src/OpenClaw.Shared -c Debug --no-restore
259+
260+
- name: Build SetupEngine
261+
run: dotnet build src/OpenClaw.SetupEngine -c Debug --no-restore
262+
263+
- name: Build Tray App (WinUI)
264+
run: dotnet build src/OpenClaw.Tray.WinUI -c Debug -r win-x64
265+
266+
- name: Build E2E Tests
267+
run: dotnet build tests/OpenClaw.E2ETests -c Debug -r win-x64
268+
269+
- name: Run E2E Tests
270+
run: >
271+
dotnet test tests/OpenClaw.E2ETests
272+
--no-build
273+
-c Debug
274+
-r win-x64
275+
--verbosity normal
276+
--results-directory TestResults/E2E
277+
--logger "trx;LogFileName=OpenClaw.E2ETests.trx"
278+
--logger "console;verbosity=detailed"
279+
280+
- name: Upload E2E Test Results & Logs
281+
if: always()
282+
uses: actions/upload-artifact@v7
283+
with:
284+
name: e2e-test-results
285+
path: |
286+
TestResults/E2E/
287+
if-no-files-found: warn
288+
225289
build:
226-
needs: test
290+
needs: [test, e2etests]
227291
runs-on: ${{ matrix.rid == 'win-arm64' && 'windows-11-arm' || 'windows-latest' }}
228292
strategy:
229293
matrix:
@@ -253,6 +317,12 @@ jobs:
253317
- name: Publish WinUI Tray App
254318
run: dotnet publish src/OpenClaw.Tray.WinUI -c Release -r ${{ matrix.rid }} --self-contained --no-restore -p:Version=${{ needs.test.outputs.semVer }} -o publish
255319

320+
- name: Publish SetupEngine.UI
321+
run: |
322+
dotnet publish src/OpenClaw.SetupEngine.UI -c Release -r ${{ matrix.rid }} --self-contained -p:Version=${{ needs.test.outputs.semVer }} -o publish-setup
323+
mkdir publish\SetupEngine
324+
copy publish-setup\* publish\SetupEngine\ -Recurse
325+
256326
- name: Disable NuGet source mapping for signing
257327
if: startsWith(github.ref, 'refs/tags/v') && matrix.rid != 'win-arm64'
258328
shell: pwsh
@@ -290,7 +360,7 @@ jobs:
290360
path: publish/
291361

292362
build-msix:
293-
needs: test
363+
needs: [test, e2etests]
294364
runs-on: ${{ matrix.rid == 'win-arm64' && 'windows-11-arm' || 'windows-latest' }}
295365
continue-on-error: true
296366
strategy:
@@ -382,7 +452,7 @@ jobs:
382452
path: ${{ steps.find-msix.outputs.msix_path }}
383453

384454
build-extension:
385-
needs: test
455+
needs: [test, e2etests]
386456
runs-on: windows-latest
387457
strategy:
388458
matrix:
@@ -416,8 +486,8 @@ jobs:
416486
path: src/OpenClaw.CommandPalette/bin/${{ matrix.platform }}/Debug/
417487

418488
release:
419-
needs: [repo-hygiene, test, build, build-msix, build-extension]
420-
if: startsWith(github.ref, 'refs/tags/v') && needs.repo-hygiene.result == 'success' && needs.test.result == 'success' && needs.build.result == 'success' && needs.build-extension.result == 'success' && !cancelled()
489+
needs: [repo-hygiene, test, e2etests, build, build-msix, build-extension]
490+
if: startsWith(github.ref, 'refs/tags/v') && needs.repo-hygiene.result == 'success' && needs.test.result == 'success' && needs.e2etests.result == 'success' && needs.build.result == 'success' && needs.build-extension.result == 'success' && !cancelled()
421491
runs-on: windows-latest
422492
permissions:
423493
contents: write

build.ps1

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
#>
2424

2525
param(
26-
[ValidateSet("All", "Tray", "WinUI", "Shared", "CommandPalette", "Cli", "WinNodeCli")]
26+
[ValidateSet("All", "Tray", "WinUI", "Shared", "CommandPalette", "Cli", "WinNodeCli", "SetupEngine")]
2727
[string]$Project = "All",
2828

2929
[ValidateSet("Debug", "Release")]
@@ -204,9 +204,10 @@ $projects = @{
204204
"Tray" = @{ Path = "src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj"; UseRid = $true }
205205
"WinUI" = @{ Path = "src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj"; UseRid = $true }
206206
"CommandPalette" = @{ Path = "src/OpenClaw.CommandPalette/OpenClaw.CommandPalette.csproj"; UseRid = $false }
207+
"SetupEngine" = @{ Path = "src/OpenClaw.SetupEngine.UI/OpenClaw.SetupEngine.UI.csproj"; UseRid = $true }
207208
}
208209

209-
$toBuild = if ($Project -eq "All") { @("Shared", "Cli", "WinNodeCli", "WinUI") } else { @($Project) }
210+
$toBuild = if ($Project -eq "All") { @("Shared", "Cli", "WinNodeCli", "SetupEngine", "WinUI") } else { @($Project) }
210211

211212
# Always build Shared first if building other projects
212213
if ($Project -ne "Shared" -and $Project -ne "All" -and $toBuild -notcontains "Shared") {
@@ -221,7 +222,23 @@ foreach ($proj in $toBuild) {
221222
}
222223

223224
# =============================================================================
224-
# SUMMARY
225+
# POST-BUILD: Copy SetupEngine.UI into WinUI output so the tray can find it
226+
# =============================================================================
227+
if (($buildResults.ContainsKey("SetupEngine") -and $buildResults["SetupEngine"]) -and
228+
(($buildResults.ContainsKey("WinUI") -and $buildResults["WinUI"]) -or ($buildResults.ContainsKey("Tray") -and $buildResults["Tray"]))) {
229+
$setupTfm = Get-ProjectTargetFramework $projects["SetupEngine"].Path
230+
$winUITfm = Get-ProjectTargetFramework $projects["WinUI"].Path
231+
if ($setupTfm -and $winUITfm) {
232+
$setupOutDir = "src\OpenClaw.SetupEngine.UI\bin\$Configuration\$setupTfm\$rid"
233+
$winUIOutDir = "src\OpenClaw.Tray.WinUI\bin\$Configuration\$winUITfm\$rid"
234+
$destDir = Join-Path $winUIOutDir "SetupEngine"
235+
if (Test-Path $setupOutDir) {
236+
if (-not (Test-Path $destDir)) { New-Item -ItemType Directory -Path $destDir -Force | Out-Null }
237+
Copy-Item "$setupOutDir\*" $destDir -Recurse -Force
238+
Write-Info "Copied SetupEngine.UI output → $destDir"
239+
}
240+
}
241+
}
225242
# =============================================================================
226243

227244
Write-Header "Build Summary"

0 commit comments

Comments
 (0)