qwen-code/packages/cua-driver/scripts/install-local.ps1
顾盼 adc2bce414
feat(cua-driver): vendor qwen-cua-driver with opt-in 0–1000 relative coordinates (#5896)
* feat(cua-driver): vendor trycua/cua driver with 1000-normalized coordinate support

Vendor libs/cua-driver from trycua/cua into packages/cua-driver as the
basis for qwen-code's computer-use backend, adding an opt-in relative
(1000x1000 normalized) coordinate mode for Qwen-VL clients.

- coord_norm.rs: 0-1000 <-> pixel conversion, per-(pid,window_id) size
  cache, tools/list description rewrite (TDD, 27 tests)
- ToolRegistry: normalized field + invoke input/output hooks
- protocol.rs: system-instruction coordinate wording switched by mode
- serve.rs: daemon list path description rewrite (input_schema aware)
- main.rs: CUA_DRIVER_RS_COORDINATE_SPACE env seed

Default coordinate_space=pixels => zero behavior change for existing
pixel clients. Set CUA_DRIVER_RS_COORDINATE_SPACE=normalized_1000 to
enable. Excludes rust/target build output.

* feat(cua-driver): make normalized coordinate scale configurable

Add CUA_DRIVER_RS_COORDINATE_SCALE (default 1000) so the normalization
full-scale can absorb the Qwen 999-vs-1000 cookbook ambiguity without a
recompile. norm_to_px/px_to_norm now take an explicit scale; denormalize_args
reads the process-wide COORDINATE_SCALE seeded once at startup from env.

* ci(cua-driver): add cross-platform release workflow for vendored driver

Standalone GitHub Action that builds, signs, and releases the vendored
cua-driver under packages/cua-driver. Adapted from upstream trycua/cua
cd-rust-cua-driver.yml:

- macOS: universal binary (lipo arm64+x86_64), codesigned + notarized into
  CuaDriver.app using qwen-code's existing secrets (MAC_CSC_LINK cert +
  App Store Connect API key notarization); Developer ID identity is
  auto-discovered from the imported cert.
- Linux: x86_64 + arm64, built in debian:11 for a glibc 2.31 floor.
- Windows: x86_64 + arm64, unsigned (no EV cert, matches upstream).
- Release: softprops/action-gh-release on cua-driver-rs-v* tags or manual
  dispatch, prerelease.

Triggered by tag push (cua-driver-rs-v*) or workflow_dispatch.

* chore(cua-driver): rebrand vendored driver as qwen-cua-driver

Rename the vendored trycua/cua driver so the fork installs and runs
independently of any upstream trycua install:
- binary cua-driver -> qwen-cua-driver
- bundle CuaDriver.app -> QwenCuaDriver.app
- bundle id com.trycua.driver -> com.qwencode.cua-driver

Updates the cargo/uia manifests, Info.plist, bundle/proxy launch paths,
permission/health-report wording, the install/build scripts, and the
cross-platform release workflow.

* feat(cua-driver): finish relative-coordinate mode — toggle, scale, zoom/move_cursor

- CUA_DRIVER_RS_COORDINATE_SPACE is now a 1/0 toggle (via is_env_truthy);
  default off keeps pixel mode byte-identical to upstream.
- Thread CUA_DRIVER_RS_COORDINATE_SCALE through every coordinate surface
  (was hardcoded 1000): input denormalization already used it; now the
  rewritten screenshot dims, the tool/param descriptions, and the agent
  instructions track the configured scale too.
- Normalize zoom (window basis) and move_cursor (screen basis) inputs and
  rewrite their descriptions, alongside click/double_click/right_click/drag.
- Fix zoom on downscaled (Retina) windows: apply the get_window_state resize
  ratio so the crop lands on the region the agent saw. Normalized mode only;
  pixel-mode zoom unchanged.

All coordinate behavior stays gated on the normalized flag, so the default
(pixels) path is unchanged from upstream.

* chore(cua-driver): add upstream-sync script (git subtree unusable here)

`git subtree split --prefix=libs/cua-driver` hangs on a commit deep in
trycua/cua's history, so the subtree add/pull workflow isn't usable for
the vendored driver (and a pull would re-split + re-hang every time).

Add scripts/sync-from-upstream.sh instead: it git-diffs two upstream refs
(never walks the full history, so it dodges the hang), reprefixes the
libs/cua-driver delta to packages/cua-driver, and `git apply --reject`s it
on top of our local changes — conflicts land as *.rej for manual fixup.
Record the vendored version in .vendored-from and document the migration +
sync method in the design doc.

* chore(cua-driver): exclude vendored driver from qwen-code ESLint

The vendored packages/cua-driver tree carries upstream JS (e.g. the
test-harness Electron app) that doesn't follow qwen-code's lint rules and
fails CI. It is not a workspace package (no package.json) and is not
qwen-code TypeScript, so add it to eslint.config.js global ignores —
alongside packages/desktop/** — the standard treatment for vendored code.

* fix(cua-driver): let start_session revive an idle-reaped session

Ports the fix from upstream trycua/cua#2035 into the vendored driver.

When a session is reaped for idleness, a subsequent start_session with the
same id failed instead of resuming it. Revive the ended session in place so
the agent can continue rather than getting a hard error.

* fix(cua-driver): retry daemon socket writes on EAGAIN

Ports the fix from upstream trycua/cua#2036 into the vendored driver.

A non-blocking daemon socket can return EAGAIN/EWOULDBLOCK mid-write when the
peer's receive buffer is momentarily full. The driver treated that as fatal
and dropped the connection. Add a bounded retry/poll loop (mirror of the
read-side socket_io helper) so transient back-pressure no longer kills the
session; only a real timeout or hard error fails the write.

* fix(cua-driver/linux): stop reporting bare "Clicked" for X11 synthetic clicks

Ports the fix from upstream trycua/cua#2025 into the vendored driver.

On X11, clicks are delivered via XSendEvent synthetic events, which many
toolkits (GTK/SDL/Allegro) ignore because send_event is set. The driver still
reported a flat success ("Clicked"), masking that nothing happened. Report
the synthetic-delivery caveat honestly so the agent can fall back instead of
assuming the click landed.

(platform-linux crate is not built on macOS; verified by clean upstream apply
and covered by upstream + release-workflow Linux CI.)

* fix(cua-driver/windows): list empty-/null-title top-level windows

Ports the fix from upstream trycua/cua#2021 into the vendored driver.

list_windows filtered out any top-level window whose title was empty or null,
so legitimate targets (splash screens, some Electron/game windows, tool
windows) were invisible to the agent and unclickable. Include empty-title
windows, using class name / process as a fallback label.

(platform-windows crate is not built on macOS; verified by clean upstream
apply and covered by upstream + release-workflow Windows CI.)

* chore(cua-driver): track cherry-picked upstream PRs; fix vendored-from

The vendored copy is actually at cua-driver-rs-v0.6.7 (workspace version and
all 0.6.7->0.6.8 delta files confirm it), but .vendored-from had drifted to
0.6.8 during an earlier sync-script trial whose code delta was not kept. Left
as-is it would make a future sync diff 0.6.8->newer and silently skip the real
0.6.7->0.6.8 fixes. Correct it back to 0.6.7.

Also record the four not-yet-merged upstream PRs we carry as cherry-picks
(trycua/cua#2021/#2025/#2035/#2036) in .vendored-patches.md, and have
sync-from-upstream.sh point at it so the next sync reconciles them.

* ci(cua-driver): satisfy repo yamllint on the release workflow

The vendored-driver release workflow tripped 114 quoted-strings violations
under the repo's .yamllint (quote-type: single, required). Single-quote all
string scalars to match every other workflow in .github/workflows.

While reformatting, the release-notes body also got its paragraph blank lines
collapsed and still referenced the old CUA_DRIVER_RS_COORDINATE_SPACE=
normalized_1000 value — restore the blank lines and update it to the current
0/1 toggle (default 0 = off; optional CUA_DRIVER_RS_COORDINATE_SCALE=1000).

* chore(cua-driver): sync vendored driver to cua-driver-rs-v0.6.8

First real run of scripts/sync-from-upstream.sh: it 3-way-applied the upstream
0.6.7->0.6.8 delta onto our local fork. 10/12 files applied cleanly; the 2
rejects (install.ps1, _install-rust.sh) were already-applied baked-version
bumps (0.6.6->0.6.7, our copies were already at 0.6.7), i.e. no real conflict.

0.6.8 brings: Wayland input path (platform-linux), linux health_report +
overlay tweaks, a platform-macos build.rs step, and dependency bumps. Version
moved to 0.6.8 across the workspace.

Verified our work survived the sync untouched: the relative-coordinate shim
(coord_norm/protocol) and all four cherry-picked PRs (socket_io/session +
linux/windows) are intact — in particular the 0.6.8 edit to platform-linux
tools/impl_.rs landed alongside our #2025 change with no collision. macOS
cargo check + 132 core tests green. (platform-linux/windows + the binary
integration test build only on their own runners; upstream CI covers those.)

* ci(cua-driver): add a dry_run gate to the release workflow

Mirror the desktop-release / release dry-run pattern: a workflow_dispatch
dry_run boolean input (default true). The cross-platform build + package jobs
always run and upload their artifacts; the GitHub Release job now publishes
only on a tag push or an explicit dry_run=false dispatch.

Lets us rehearse the whole build/package pipeline (dry_run=true, notarize=false)
and inspect the produced artifacts without cutting a release. A branch push
(no tag, not a dispatch) likewise builds without releasing.
2026-06-26 13:06:43 +00:00

375 lines
17 KiB
PowerShell

# cua-driver-rs local installer (Windows). Builds release-mode from the
# current source tree and drops the resulting cua-driver.exe into the same
# install layout that scripts/install.ps1 produces — so a local build and
# a release install can coexist + the `current` junction can flip between
# them.
#
# Params mirror scripts/install.ps1 so the developer loop matches what
# end users experience:
# -AutoStart register the cua-driver-serve Scheduled Task at logon
# (Windows-native equivalent of macOS LaunchAgent).
# Default off; the post-install message prints the
# registration recipe so you can opt in later.
# -NoPathUpdate skip the auto-append of the bin dir to the User PATH.
# Mirrors install.ps1's flag.
#
# Always builds in `release` configuration to match what install.ps1
# hands users (the prebuilt zip from GitHub Releases is `--release`).
# Use `cargo build -p cua-driver` directly + invoke target\debug\cua-driver.exe
# if you specifically want a faster-to-compile debug binary.
#
# Not for end-users — `irm https://.../install.ps1 | iex` fetches a
# signed/built release from GitHub. This script is for the developer
# loop (rapid edit/build/test on a Windows host).
#
# Layout produced (matches install.ps1 — see its header for details):
#
# <visibleBinDir> [junction → currentDir]
# <currentDir> [junction → release dir, retargeted here]
# <release dir> [real dir, this script's output]
# 0.0.0-local-release-<target>\cua-driver.exe
#
# The version-string carries `-local-release` so it never collides with
# a real release dir and is trivial to garbage-collect.
[CmdletBinding()]
param(
# Default-on: most users want the daemon to come back at every
# logon. Opt out with `-AutoStart:$false` (or the dedicated
# `-NoAutoStart`) when running install-local.ps1 from CI / a
# container build / a sandbox where you specifically don't want
# a scheduled task registered.
[switch]$AutoStart = $true,
[switch]$NoAutoStart,
[switch]$NoPathUpdate
)
# `-NoAutoStart` is the explicit opt-out; takes precedence over the
# default-true `-AutoStart` for callers who'd rather read negative
# than `-AutoStart:$false`.
if ($NoAutoStart) { $AutoStart = $false }
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
# Reuse the production installer's helpers (path resolution, junction
# wiring, autostart registration) by dot-sourcing the relevant bits via
# a small wrapper. install.ps1 expects to run end-to-end, so we don't
# dot-source the whole thing — instead duplicate the small handful of
# operations we need, calling out matching install.ps1 functions where
# the logic would otherwise drift.
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
# Rust workspace root: scripts/ is the cross-cutting installer dir at
# libs/cua-driver/scripts/; the Cargo workspace lives one level deeper
# under libs/cua-driver/rust/.
$RepoRoot = (Resolve-Path "$ScriptDir\..\rust").Path
$BinaryName = "cua-driver.exe"
# Always release-config — matches the binary install.ps1 hands end users.
$Config = "release"
# Arch detection — use $env:PROCESSOR_ARCHITECTURE rather than
# RuntimeInformation::OSArchitecture so this works under
# Set-StrictMode -Version Latest (same fix as install.ps1 PR #1631).
$archEnv = $env:PROCESSOR_ARCHITECTURE
$Target = switch -Regex ($archEnv) {
'^ARM64$' { "aarch64-pc-windows-msvc"; break }
default { "x86_64-pc-windows-msvc" }
}
# ---------- Paths (must match install.ps1's defaults) ----------------------
if ($env:CUA_DRIVER_RS_INSTALL_DIR) {
$VisibleBinDir = $env:CUA_DRIVER_RS_INSTALL_DIR
} else {
# Path layout matches install.ps1's v0.2.14+ rename
# (trycua\cua-driver-rs → Cua\cua-driver). See PR #1644.
$VisibleBinDir = Join-Path $env:LOCALAPPDATA "Programs\Cua\cua-driver\bin"
}
if ($env:CUA_DRIVER_RS_HOME) {
$PackageHome = $env:CUA_DRIVER_RS_HOME
} else {
$PackageHome = Join-Path $env:USERPROFILE ".cua-driver"
}
$CurrentDir = Join-Path $PackageHome "packages\current"
$ReleasesDir = Join-Path $PackageHome "packages\releases"
# ---------- Helpers (mirror install.ps1) ----------------------------------
#
# install.ps1 keeps these inline in its own scope (it's a one-shot script
# that runs end-to-end). Mirror them here so install-local.ps1 can stand
# alone too. If install.ps1 ever extracts these into a shared file, this
# duplication is the time to delete it.
function Test-IsJunction([string]$path) {
if (-not (Test-Path -LiteralPath $path)) { return $false }
$item = Get-Item -LiteralPath $path -Force
return [bool]($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint)
}
function Ensure-Junction([string]$linkPath, [string]$targetPath) {
New-Item -ItemType Directory -Path (Split-Path -Parent $linkPath) -Force | Out-Null
if (Test-Path -LiteralPath $linkPath) {
if (Test-IsJunction $linkPath) {
# Always retarget — that's the point of this helper.
cmd /c rmdir (Resolve-Path -LiteralPath $linkPath).Path | Out-Null
} else {
throw "Refusing to replace non-junction at $linkPath. Move or delete it first."
}
}
cmd /c mklink /J $linkPath $targetPath | Out-Null
}
function Register-CuaDriverAutostart {
param([Parameter(Mandatory = $true)][string]$InstalledBinary)
if (-not (Test-Path -LiteralPath $InstalledBinary)) {
throw "binary not found at $InstalledBinary"
}
& $InstalledBinary autostart enable
if ($LASTEXITCODE -ne 0) {
throw "cua-driver autostart enable failed (exit $LASTEXITCODE)"
}
}
# Stop-CuaDriverDaemons + Show-CuaDriverDaemonSurvivors are defined in
# the sibling _install-common.psm1 module - shared with install.ps1
# so the daemon-cleanup logic stays in one place. Local dev runs from
# a checked-out tree, so we always have the file on disk.
Import-Module -Name (Join-Path $ScriptDir "_install-common.psm1") -Force
function Write-Step($msg) { Write-Host "==> $msg" -ForegroundColor Cyan }
# ---------- Prerequisites --------------------------------------------------
Write-Step "cua-driver-rs local installer (Windows)"
Write-Host " source: $RepoRoot"
Write-Host " config: $Config"
Write-Host " target: $Target"
Write-Host " visible: $VisibleBinDir"
Write-Host " current: $CurrentDir"
if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) {
Write-Host "Error: cargo not found on PATH." -ForegroundColor Red
Write-Host "Install Rust + MSVC toolchain via rustup-init: https://rustup.rs/"
exit 1
}
# ---------- Build ----------------------------------------------------------
Write-Step "cargo build --release -p cua-driver"
Push-Location $RepoRoot
try {
& cargo build --release -p cua-driver
if ($LASTEXITCODE -ne 0) {
Write-Host "Error: cargo build failed." -ForegroundColor Red
exit $LASTEXITCODE
}
}
finally {
Pop-Location
}
$BuiltBinary = Join-Path $RepoRoot "target\$Config\$BinaryName"
if (-not (Test-Path -LiteralPath $BuiltBinary)) {
Write-Host "Error: build produced no binary at $BuiltBinary" -ForegroundColor Red
exit 1
}
# ---------- Stage into versioned release dir -------------------------------
$VersionTag = "0.0.0-local-$Config"
$VersionedDir = Join-Path $ReleasesDir "$VersionTag-$Target"
$DestBinary = Join-Path $VersionedDir $BinaryName
# If a previous install-local left a binary here and it's currently
# being executed (typical: `cua-driver autostart kick` spawned a
# High-IL daemon at logon, which we can't terminate from this
# Medium-IL shell without UAC), the Copy-Item below fails with
# "The process cannot access the file ... because it is being used by
# another process." Windows DOES allow renaming a locked .exe — the
# loader opens images with FILE_SHARE_DELETE, so a rename succeeds
# while the content stays locked. Renaming out of the way frees up
# the destination path so Copy-Item lands cleanly. The old file gets
# unlinked at the next reboot or when the daemon exits.
if (Test-Path -LiteralPath $DestBinary) {
$ts = (Get-Date).ToString('yyyyMMdd-HHmmss')
$stale = "$DestBinary.stale-$ts"
try {
Move-Item -LiteralPath $DestBinary -Destination $stale -Force -ErrorAction Stop
Write-Step "renamed locked previous binary to $(Split-Path -Leaf $stale)"
} catch {
Write-Host "Note: could not rename previous binary at $DestBinary." -ForegroundColor Yellow
Write-Host " ($($_.Exception.Message))" -ForegroundColor Yellow
Write-Host " Most likely a running cua-driver daemon is holding it." -ForegroundColor Yellow
Write-Host " Stop it first (e.g. ``schtasks /End /TN cua-driver-serve`` then re-run)." -ForegroundColor Yellow
}
# Best-effort GC of stale-* siblings older than this run. Cheap;
# keeps the dir from growing unbounded over many re-builds.
Get-ChildItem -LiteralPath $VersionedDir -Filter "$BinaryName.stale-*" -ErrorAction SilentlyContinue |
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-1) } |
ForEach-Object { try { Remove-Item -LiteralPath $_.FullName -Force -ErrorAction SilentlyContinue } catch {} }
Write-Step "killing previous cua-driver processes (best-effort; High-IL needs admin)"
# Repair- variant does Stop-CuaDriverDaemonsWithHealth + stale
# detection + UAC self-elevation when wedged: if survivors are
# present AND the pipe is dead, prompts the user (y/n), and on
# yes triggers a UAC prompt to spawn a brief elevated
# PowerShell that kills the High-IL pids and re-runs the
# scheduled task. On UAC accept + healthy pipe afterward, the
# install proceeds at Medium IL like nothing happened. On UAC
# cancel / failure, falls back to the same printed manual
# recovery instructions the previous flow used.
$null = Repair-CuaDriverStaleDaemon
}
Write-Step "staging into $VersionedDir"
New-Item -ItemType Directory -Path $VersionedDir -Force | Out-Null
Copy-Item -LiteralPath $BuiltBinary -Destination $DestBinary -Force
$installedBinary = $DestBinary
# Stage the skill pack alongside the binary. install-local mirrors what
# install.ps1 does from a release zip — copies Skills/cua-driver-rs/ from
# the repo into the versioned dir so the `current` junction below
# transparently exposes it to agents.
$SourceSkills = Join-Path $RepoRoot "Skills\cua-driver-rs"
if (Test-Path -LiteralPath $SourceSkills) {
$StagedSkills = Join-Path $VersionedDir "Skills\cua-driver-rs"
if (Test-Path -LiteralPath $StagedSkills) {
Remove-Item -LiteralPath $StagedSkills -Recurse -Force
}
New-Item -ItemType Directory -Path (Split-Path -Parent $StagedSkills) -Force | Out-Null
Copy-Item -Path $SourceSkills -Destination $StagedSkills -Recurse -Force
Write-Step "staged skill pack at $StagedSkills"
}
# ---------- Repoint junctions ---------------------------------------------
Write-Step "retargeting $CurrentDir -> $VersionedDir"
Ensure-Junction -linkPath $CurrentDir -targetPath $VersionedDir
Write-Step "ensuring $VisibleBinDir -> $CurrentDir"
if (Test-Path -LiteralPath $VisibleBinDir) {
if (-not (Test-IsJunction $VisibleBinDir)) {
Write-Host "$VisibleBinDir exists and is not a junction; aborting." -ForegroundColor Red
Write-Host "Remove or relocate it, then re-run."
exit 1
}
}
Ensure-Junction -linkPath $VisibleBinDir -targetPath $CurrentDir
# ---------- User PATH update (matches install.ps1) ------------------------
if (-not $NoPathUpdate) {
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
$alreadyOnPath = $userPath -and (($userPath -split ';') -contains $VisibleBinDir)
if (-not $alreadyOnPath) {
$newValue = if ($userPath) { ($userPath.TrimEnd(';')) + ';' + $VisibleBinDir } else { $VisibleBinDir }
[Environment]::SetEnvironmentVariable('Path', $newValue, 'User')
# Also update the current process's $env:Path so subsequent
# commands in THIS shell see cua-driver immediately. install.ps1
# does the same — see #1651.
if (-not (($env:Path -split ';') -contains $VisibleBinDir)) {
$env:Path = "$VisibleBinDir;$env:Path"
}
Write-Step "added $VisibleBinDir to User PATH"
} else {
Write-Step "$VisibleBinDir already on User PATH"
}
} else {
Write-Step "skipping User PATH update (-NoPathUpdate)"
}
# Agent skill pack symlinks: NOT auto-created. Run
# `cua-driver skills install --local` to symlink agent dirs to the
# staged copy at $StagedSkills above.
# ---------- Done -----------------------------------------------------------
Write-Host ""
Write-Step "installed"
Write-Host " exe: $(Join-Path $VisibleBinDir $BinaryName)"
Write-Host " source: $installedBinary"
Write-Host ""
if ($AutoStart) {
Write-Step "registering Scheduled Task 'cua-driver-serve'"
try {
Register-CuaDriverAutostart -InstalledBinary (Join-Path $VisibleBinDir $BinaryName)
Write-Host " Registered. cua-driver serve auto-starts at every interactive logon." -ForegroundColor Green
}
catch {
Write-Host " Failed to register: $($_.Exception.Message)" -ForegroundColor Red
}
} else {
# User didn't pass -AutoStart, but if a `cua-driver-serve` task is
# ALREADY registered (from a previous `install.ps1 -AutoStart` or
# `cua-driver autostart enable`), re-register it pointing at this
# fresh binary. Otherwise the user ends up with a task whose
# <Command> path is the OLD release-install dir, running the OLD
# binary - even though `cua-driver` on PATH now resolves to the
# fresh one. See trycua/cua#1654 (hidden-console wrapper landed
# later - old tasks that survived an upgrade still produce the
# visible console window at logon).
$prevEAP = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try {
& schtasks.exe /Query /TN "cua-driver-serve" 2>$null | Out-Null
$hasTask = ($LASTEXITCODE -eq 0)
} finally {
$ErrorActionPreference = $prevEAP
}
if ($hasTask) {
Write-Step "found existing 'cua-driver-serve' task - re-registering against fresh binary"
try {
Register-CuaDriverAutostart -InstalledBinary (Join-Path $VisibleBinDir $BinaryName)
Write-Host " Re-registered. Task action now uses this build's hidden-console wrapper." -ForegroundColor Green
}
catch {
Write-Host " Failed to re-register: $($_.Exception.Message)" -ForegroundColor Red
Write-Host " The existing task still points at the previous binary. Run 'cua-driver autostart enable' from an elevated shell to update."
}
}
}
# Unified post-install hints come from a single shared text file so the
# 4 Rust installers (this script + install-local.sh + install.ps1 +
# _install-rust.sh) never drift. The .txt holds the OS-agnostic bulk
# (Try-it / skill pack / MCP setup / docs link) with {{BINARY}}
# placeholders; OS-specific bits stay inline below.
$installedBinary = Join-Path $VisibleBinDir $BinaryName
$HintsTxt = Join-Path $ScriptDir "post-install-hints.txt"
if (Test-Path -LiteralPath $HintsTxt) {
# Read explicitly as UTF-8. PowerShell 5.1's Get-Content -Raw falls
# back to Windows-1252 when the source file has no BOM, which turns
# the .txt's `•` / `—` into mojibake (`•` / `â€"`) in the rendered
# block. The other 3 installers don't hit this: install.ps1 reads
# the URL response via Invoke-WebRequest (HTTP charset decoding);
# _install-rust.sh / install-local.sh stream raw bytes through sed.
$hintsRaw = [System.IO.File]::ReadAllText($HintsTxt, [System.Text.Encoding]::UTF8)
Write-Host ($hintsRaw -replace '\{\{BINARY\}\}', $installedBinary)
} else {
# Repo layout changed or .txt missing — fall back to one-line
# essentials so users still know what to do next.
Write-Host "Next steps: $installedBinary --version | $installedBinary mcp-config | $installedBinary skills install"
Write-Host "Docs: https://github.com/trycua/cua/tree/main/libs/cua-driver/rust"
}
# Windows-specific autostart hint (kept inline; per-shell natural location).
if ($AutoStart) {
# Default branch: autostart was enabled (either by default or explicitly).
# Surface the management subcommands so the user knows how to inspect /
# disable later without digging through Task Scheduler.
Write-Host ""
Write-Host "Auto-start: 'cua-driver-serve' is registered at RunLevel=Highest." -ForegroundColor Cyan
Write-Host " cua-driver autostart status (inspect)" -ForegroundColor Cyan
Write-Host " cua-driver autostart disable (remove)" -ForegroundColor Cyan
Write-Host " cua-driver autostart kick (start now without re-logging)" -ForegroundColor Cyan
Write-Host ""
} else {
# Opt-out branch (-NoAutoStart or -AutoStart:`$false`).
Write-Host ""
Write-Host "Auto-start at logon (NOT enabled - re-run without -NoAutoStart to register, or:):" -ForegroundColor Cyan
Write-Host " cua-driver autostart enable (register Scheduled Task at RunLevel=Highest)" -ForegroundColor Cyan
Write-Host " cua-driver autostart kick (start now without re-logging)" -ForegroundColor Cyan
Write-Host " cua-driver autostart status (inspect)" -ForegroundColor Cyan
Write-Host " cua-driver autostart disable (remove)" -ForegroundColor Cyan
Write-Host ""
}