Files
wehub-resource-sync 91e75e620b
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Has been cancelled
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Has been cancelled
CD: Docs MCP Server / build (linux/amd64) (push) Has been cancelled
CD: Docs MCP Server / build (linux/arm64) (push) Has been cancelled
CD: Docs MCP Server / merge (push) Has been cancelled
CI: cua-driver distro-compat matrix / Resolve release version (push) Has been cancelled
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Has been cancelled
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Has been cancelled
CI: cua-driver distro-compat matrix / Distro compat summary (push) Has been cancelled
CI: Rust Linux unit / Rust Linux unit and compile (push) Has been cancelled
CI: Rust Windows unit / Rust Windows unit and compile (push) Has been cancelled
CI: Nix Linux Rust source / Nix / compositor build (push) Has been cancelled
CI: Nix Linux Rust source / Nix / driver package (push) Has been cancelled
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:19 +08:00

446 lines
21 KiB
PowerShell

# cua-driver-rs uninstaller (Windows) — removes everything install.ps1
# laid down: the Scheduled Task autostart entry, running daemon
# processes, the directory junctions wiring the visible bin dir back to
# a per-version release dir, the entire package home tree, and any skill
# junctions the binary's `skills install` verb dropped under the agent
# config directories.
#
# Usage (one-liner — recommended):
# irm https://cua.ai/driver/uninstall.ps1 | iex
#
# Force (skip all prompts):
# $env:CUA_DRIVER_RS_UNINSTALL_FORCE = '1'
# irm https://cua.ai/driver/uninstall.ps1 | iex
#
# Why an env var and not a `-Force` parameter: `iex` (Invoke-Expression)
# refuses to parse a script that opens with `[CmdletBinding()] param(...)`
# — the natural one-liner above otherwise fails with
# "Unexpected attribute 'CmdletBinding'" at parse time. Reading the flag
# from $env: keeps the body iex-safe, and the env var inherits across
# the self-elevation re-exec for free (no -ArgumentList forwarding).
#
# What gets removed:
# - Scheduled Task 'cua-driver-serve' (autostart entry registered by
# `cua-driver autostart enable` or install.ps1 -AutoStart)
# - Any running cua-driver.exe processes (so file handles don't pin
# the binary directory open during the delete pass)
# - <visibleBinDir> = %LOCALAPPDATA%\Programs\Cua\cua-driver\bin (directory junction)
# - <currentDir> = %USERPROFILE%\.cua-driver\packages\current (directory junction)
# - <packageHome> = %USERPROFILE%\.cua-driver\ (entire tree:
# releases, lockfile,
# telemetry id,
# install marker,
# version_check.json)
# - Skill junctions under:
# %USERPROFILE%\.claude\skills\cua-driver-rs
# %USERPROFILE%\.agents\skills\cua-driver-rs
# %USERPROFILE%\.openclaw\skills\cua-driver-rs
# %APPDATA%\opencode\skills\cua-driver-rs
# (each only when it's a reparse point — never clobber a real dir).
#
# Conservative on Claude MCP cleanup: we DON'T auto-edit %USERPROFILE%\
# .claude.json on Windows (mirrors the macOS uninstall.sh's stance for
# environments without python3). The closing message prints the
# `claude mcp remove cua-driver-rs` command for the user to run.
#
# Env overrides (mirror install.ps1's variable names):
# $env:CUA_DRIVER_RS_INSTALL_DIR visible bin dir to remove
# (default %LOCALAPPDATA%\Programs\Cua\cua-driver\bin;
# v0.2.13 and earlier used Programs\trycua\cua-driver-rs\bin
# — that legacy path is always cleaned up too)
# $env:CUA_DRIVER_RS_HOME package home to remove
# (default %USERPROFILE%\.cua-driver;
# v0.2.13 and earlier used .cua-driver-rs —
# that legacy path is always cleaned up too)
#
# Env (force mode):
# $env:CUA_DRIVER_RS_UNINSTALL_FORCE
# non-interactive: skip the "remove? [y/N]" prompt before
# each major delete. The one-liner is interactive by
# default so a stray paste doesn't accidentally wipe a
# working install. Inherited automatically by the elevated
# re-exec child (see Elevation below).
#
# Elevation:
# `install.ps1 -AutoStart` (and `cua-driver autostart enable`) register
# the `cua-driver-serve` Scheduled Task at RunLevel=Highest — the
# daemon spawned by it then runs at High IL so it can drive UWP /
# AppContainer apps (Calculator, Settings, Photos — see
# autostart.rs:127). Side-effect: a non-elevated process (even the same
# user that installed it) can NOT terminate the daemon or delete the
# task — both fail with Access Denied, and the binary stays locked
# under ~\.cua-driver\... . If we detect either condition at startup
# we self-elevate via UAC; otherwise we run in-place. Mirrors the
# install side's elevation pattern in autostart.rs:215-223.
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
# $Force is read from the environment so the script body stays iex-safe.
# `[CmdletBinding()]` / `param()` declarations are only legal at the start
# of a script-file, function, or scriptblock — NOT inside an
# Invoke-Expression'd string — so `irm <url> | iex` parses with
# "Unexpected attribute 'CmdletBinding'" if we keep a formal param block.
# Env var also inherits across the self-elevation re-exec automatically,
# so we don't need to plumb -Force through -ArgumentList.
$Force = [bool]$env:CUA_DRIVER_RS_UNINSTALL_FORCE
# ---------- Elevation pre-check -------------------------------------------
$AutoStartTask = "cua-driver-serve"
function Test-IsElevated {
([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
function Test-NeedsElevation {
# Either the autostart task exists (RunLevel=Highest, so deleting it
# needs admin) or a cua-driver.exe is running (its parent was the
# elevated task, so terminating it needs admin too). Detecting either
# upfront lets us self-elevate before we start tearing things down
# — otherwise the non-elevated path silently swallows access-denied
# from schtasks /Delete + Stop-Process and leaves a dangling install.
$prevEAP = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try {
& schtasks.exe /Query /TN $AutoStartTask 2>$null | Out-Null
$hasTask = ($LASTEXITCODE -eq 0)
} finally {
$ErrorActionPreference = $prevEAP
}
$hasDaemon = @(Get-Process -Name "cua-driver" -ErrorAction SilentlyContinue).Count -gt 0
return ($hasTask -or $hasDaemon)
}
if (-not (Test-IsElevated) -and (Test-NeedsElevation)) {
Write-Host "==> cua-driver-rs uninstaller: detected -AutoStart install state" -ForegroundColor Cyan
Write-Host " (the 'cua-driver-serve' task is RunLevel=Highest and/or a daemon is"
Write-Host " running at High IL). Removing them needs admin — triggering UAC prompt."
# Re-exec self elevated. $MyInvocation.MyCommand.Path is set when invoked
# from a file on disk; empty when piped through `irm ... | iex` (the
# canonical one-liner). For the iex case we materialize the script body
# to a tempfile and re-exec from there — RunAs needs a file path.
#
# No -Force forwarding needed: $env:CUA_DRIVER_RS_UNINSTALL_FORCE
# already lives in this process's env if the user opted in, and
# Start-Process inherits the current env into the elevated child.
$scriptPath = $MyInvocation.MyCommand.Path
if (-not $scriptPath) {
$tmp = Join-Path $env:TEMP ("cua-driver-uninstall-" + [Guid]::NewGuid().ToString('N') + ".ps1")
$body = $MyInvocation.MyCommand.Definition
Set-Content -LiteralPath $tmp -Value $body -Encoding UTF8
$scriptPath = $tmp
}
$argList = @('-ExecutionPolicy', 'Bypass', '-NoProfile', '-File', $scriptPath)
try {
$proc = Start-Process -FilePath powershell.exe -ArgumentList $argList -Verb RunAs -PassThru -Wait -ErrorAction Stop
exit $proc.ExitCode
} catch {
Write-Host "error: failed to elevate ($($_.Exception.Message))" -ForegroundColor Red
Write-Host " Re-run this script from an elevated PowerShell instead:" -ForegroundColor Yellow
Write-Host " Right-click PowerShell → Run as Administrator, then re-run the uninstall." -ForegroundColor Yellow
exit 1
}
}
# ---------- Path resolution (mirrors install.ps1) -------------------------
if ($env:CUA_DRIVER_RS_INSTALL_DIR) {
$VisibleBinDir = $env:CUA_DRIVER_RS_INSTALL_DIR
} else {
# New layout (v0.2.14+). Path rename rationale: see install.ps1.
$VisibleBinDir = Join-Path $env:LOCALAPPDATA "Programs\Cua\cua-driver\bin"
}
# Legacy bin dir from v0.2.13 and earlier. We also clean these up so a
# fresh uninstall after upgrading leaves nothing behind. Empty-vendor-dir
# (Programs\trycua\) gets pruned if no other apps live under it.
$LegacyVisibleBinDir = Join-Path $env:LOCALAPPDATA "Programs\trycua\cua-driver-rs\bin"
$LegacyVendorDir = Join-Path $env:LOCALAPPDATA "Programs\trycua"
if ($env:CUA_DRIVER_RS_HOME) {
$HomeDir = $env:CUA_DRIVER_RS_HOME
} else {
$HomeDir = Join-Path $env:USERPROFILE ".cua-driver"
}
# Legacy package home from v0.2.13 and earlier.
$LegacyHomeDir = Join-Path $env:USERPROFILE ".cua-driver-rs"
$PackagesDir = Join-Path $HomeDir "packages"
$CurrentDir = Join-Path $PackagesDir "current"
# $AutoStartTask hoisted to the elevation pre-check block above.
# Skill junctions — mirrors the AGENTS list in
# libs/cua-driver/rust/crates/cua-driver/src/skills.rs (the verb that
# creates them) so we remove from the same paths. Both the current
# `cua-driver` name and the pre-rename `cua-driver-rs` name are swept
# so a user who installed before the rename ends up clean.
$SkillJunctions = @(
(Join-Path $env:USERPROFILE ".claude\skills\cua-driver"),
(Join-Path $env:USERPROFILE ".agents\skills\cua-driver"),
(Join-Path $env:USERPROFILE ".openclaw\skills\cua-driver"),
(Join-Path $env:APPDATA "opencode\skills\cua-driver"),
(Join-Path $env:USERPROFILE ".gemini\skills\cua-driver"),
(Join-Path $env:USERPROFILE ".hermes\skills\cua-driver"),
(Join-Path $env:USERPROFILE ".claude\skills\cua-driver-rs"),
(Join-Path $env:USERPROFILE ".agents\skills\cua-driver-rs"),
(Join-Path $env:USERPROFILE ".openclaw\skills\cua-driver-rs"),
(Join-Path $env:APPDATA "opencode\skills\cua-driver-rs"),
(Join-Path $env:USERPROFILE ".gemini\skills\cua-driver-rs"),
(Join-Path $env:USERPROFILE ".hermes\skills\cua-driver-rs")
)
# ---------- Log helpers ----------------------------------------------------
function Write-Step($message) {
Write-Host "==> $message"
}
function Write-WarningStep($message) {
Write-Host "WARNING: $message" -ForegroundColor Yellow
}
function Write-ErrorStep($message) {
Write-Host "error: $message" -ForegroundColor Red
}
# ---------- Reparse-point helpers -----------------------------------------
#
# install.ps1 wires the bin\ and current\ directories with NTFS directory
# junctions (IO_REPARSE_TAG_MOUNT_POINT). Test-IsReparsePoint differentiates
# them from real directories so we only ever Remove-Item a path the
# installer could have created — never clobber a user's hand-managed dir.
function Test-IsReparsePoint([string]$path) {
if (-not (Test-Path -LiteralPath $path)) { return $false }
try {
$item = Get-Item -LiteralPath $path -Force -ErrorAction Stop
} catch {
return $false
}
return (($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0)
}
# ---------- Confirmation prompt -------------------------------------------
#
# The one-liner runs interactive by default so a stray paste doesn't wipe
# a working install — Confirm-Remove gates each major delete. -Force
# (passed at param parse time) skips every prompt, which is what CI / a
# scripted teardown wants.
function Confirm-Remove([string]$what) {
if ($Force) { return $true }
# PowerShell's Host.UI prompt handles non-interactive shells (e.g.
# piped from `irm | iex` in some hosts) by reading from stdin —
# which is the same channel the script body was just piped through.
# Fall back to treating an empty / non-y response as "no" so the
# default is safe.
$resp = Read-Host "Remove $what ? [y/N]"
return ($resp -match '^(y|yes)$')
}
# ---------- Main -----------------------------------------------------------
Write-Step "cua-driver-rs uninstaller (Windows)"
Write-Step " bin dir : $VisibleBinDir"
Write-Step " package home: $HomeDir"
Write-Host ""
# 1. Scheduled Task autostart (registered by `cua-driver autostart enable`
# or install.ps1 -AutoStart). Idempotent — schtasks /Query returns
# non-zero AND writes stderr when the task is absent. Under PS 5.1 with
# $ErrorActionPreference=Stop (set at the top of this script), native
# command stderr becomes a terminating error even when we redirect with
# `2>$null` — the redirect suppresses display but the error record is
# still emitted into the error stream. Locally lower ErrorActionPreference
# around the native call so the missing-task case is non-fatal.
$prevEAP = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try {
$taskQuery = & schtasks.exe /Query /TN $AutoStartTask 2>$null
$taskExitCode = $LASTEXITCODE
} finally {
$ErrorActionPreference = $prevEAP
}
if ($taskExitCode -eq 0 -and $taskQuery) {
if (Confirm-Remove "scheduled task '$AutoStartTask' (autostart at logon)") {
$ErrorActionPreference = 'Continue'
try {
& schtasks.exe /Delete /TN $AutoStartTask /F 2>$null | Out-Null
$delExit = $LASTEXITCODE
} finally {
$ErrorActionPreference = $prevEAP
}
if ($delExit -eq 0) {
Write-Step "removed scheduled task $AutoStartTask"
} else {
Write-WarningStep "schtasks /Delete /TN $AutoStartTask returned $delExit"
}
} else {
Write-Step "skipped scheduled task $AutoStartTask (user declined)"
}
} else {
Write-Step "no scheduled task '$AutoStartTask' registered (skipping)"
}
# 2. Running cua-driver.exe processes. The serve daemon and any active
# `cua-driver` invocation hold file handles to the binary, which
# pin the directory junction's target open and make Remove-Item
# fail with "in use". Stop them up front so subsequent deletes
# aren't racy.
$running = @(Get-Process -Name "cua-driver" -ErrorAction SilentlyContinue)
if ($running.Count -gt 0) {
Write-Step "stopping $($running.Count) running cua-driver.exe process(es)"
foreach ($p in $running) {
try {
Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue
} catch {
Write-WarningStep "could not stop pid $($p.Id): $($_.Exception.Message)"
}
}
# Brief pause so the kernel finishes tearing down the process and
# releases its file handles before we try to delete the binary dir.
Start-Sleep -Milliseconds 250
} else {
Write-Step "no running cua-driver.exe processes"
}
# 3. Visible bin directory junction. Only remove when it's actually a
# reparse point — refuse to clobber a real directory the user might
# have at that path.
if (Test-Path -LiteralPath $VisibleBinDir) {
if (Test-IsReparsePoint $VisibleBinDir) {
if (Confirm-Remove "directory junction $VisibleBinDir") {
# Remove-Item on a reparse point removes the reparse point
# itself, NOT the contents of the target. -Force is needed
# to delete a non-empty junction; -Recurse is harmless
# against a junction (we're removing the link, not the
# tree it points to).
Remove-Item -LiteralPath $VisibleBinDir -Force -Recurse -ErrorAction SilentlyContinue
Write-Step "removed junction $VisibleBinDir"
} else {
Write-Step "skipped $VisibleBinDir (user declined)"
}
} else {
Write-WarningStep "$VisibleBinDir exists but is not a reparse point — refusing to remove."
Write-WarningStep " install.ps1 only creates junctions at this path, so this is likely a hand-managed directory."
}
} else {
Write-Step "no junction at $VisibleBinDir (skipping)"
}
# 4. current\ directory junction inside the package home. Same
# reparse-point check as above — never clobber a real dir.
if (Test-Path -LiteralPath $CurrentDir) {
if (Test-IsReparsePoint $CurrentDir) {
Remove-Item -LiteralPath $CurrentDir -Force -Recurse -ErrorAction SilentlyContinue
Write-Step "removed junction $CurrentDir"
} else {
Write-WarningStep "$CurrentDir exists but is not a reparse point — leaving it for the package-home pass below."
}
}
# 5. Entire package home ($HomeDir). Contains releases\, lockfile,
# telemetry id, install marker, version_check.json, and the now-
# removed current\ junction.
if (Test-Path -LiteralPath $HomeDir) {
if (Confirm-Remove "package home tree $HomeDir (releases, lockfile, telemetry id, install marker)") {
# -Recurse -Force walks into every subdir and clears read-only
# bits. ErrorAction SilentlyContinue tolerates leftover handles
# (rare after step 2's process kill); we log a follow-up if
# anything survived.
Remove-Item -LiteralPath $HomeDir -Force -Recurse -ErrorAction SilentlyContinue
if (Test-Path -LiteralPath $HomeDir) {
Write-WarningStep "$HomeDir was not fully removed — some files may still be locked."
Write-WarningStep " Close any open cua-driver processes / shells with cwd inside the tree and re-run."
} else {
Write-Step "removed $HomeDir"
}
} else {
Write-Step "skipped $HomeDir (user declined)"
}
} else {
Write-Step "no package home at $HomeDir (skipping)"
}
# 6. Legacy install layout from v0.2.13 and earlier
# (`Programs\trycua\cua-driver-rs\` + `.cua-driver-rs\`). We always
# sweep these so a fresh uninstall after upgrading via install.ps1
# leaves nothing behind. Skip silently when the legacy paths don't
# exist — common case post-v0.2.14.
if (Test-Path -LiteralPath $LegacyVisibleBinDir) {
if (Test-IsReparsePoint $LegacyVisibleBinDir) {
Remove-Item -LiteralPath $LegacyVisibleBinDir -Force -Recurse -ErrorAction SilentlyContinue
Write-Step "removed legacy junction $LegacyVisibleBinDir"
} else {
Remove-Item -LiteralPath $LegacyVisibleBinDir -Force -Recurse -ErrorAction SilentlyContinue
Write-Step "removed legacy directory $LegacyVisibleBinDir"
}
}
# Empty cua-driver-rs parent (under trycua\)
$legacyParent = Split-Path -Parent $LegacyVisibleBinDir
if ((Test-Path -LiteralPath $legacyParent) -and -not (Get-ChildItem -LiteralPath $legacyParent -Force -ErrorAction SilentlyContinue)) {
Remove-Item -LiteralPath $legacyParent -Force -ErrorAction SilentlyContinue
Write-Step "removed empty legacy parent $legacyParent"
}
# Empty trycua vendor dir
if ((Test-Path -LiteralPath $LegacyVendorDir) -and -not (Get-ChildItem -LiteralPath $LegacyVendorDir -Force -ErrorAction SilentlyContinue)) {
Remove-Item -LiteralPath $LegacyVendorDir -Force -ErrorAction SilentlyContinue
Write-Step "removed empty legacy vendor dir $LegacyVendorDir"
}
# Legacy package home
if (Test-Path -LiteralPath $LegacyHomeDir) {
Remove-Item -LiteralPath $LegacyHomeDir -Force -Recurse -ErrorAction SilentlyContinue
if (Test-Path -LiteralPath $LegacyHomeDir) {
Write-WarningStep "$LegacyHomeDir was not fully removed — some files may still be locked."
} else {
Write-Step "removed legacy package home $LegacyHomeDir"
}
}
# 7. Skill junctions. Only remove reparse points — leave a real dir
# in place (a user with a hand-managed cua-driver-rs skill dir
# gets to keep it). Same defensive shape as Linux/macOS.
foreach ($skillLink in $SkillJunctions) {
if (Test-Path -LiteralPath $skillLink) {
if (Test-IsReparsePoint $skillLink) {
Remove-Item -LiteralPath $skillLink -Force -Recurse -ErrorAction SilentlyContinue
Write-Step "removed skill junction $skillLink"
} else {
Write-Step "$skillLink is a real directory (not a reparse point) — skipping"
}
} else {
Write-Step "no skill junction at $skillLink (skipping)"
}
}
# ---------- Closing message -----------------------------------------------
Write-Host ""
Write-Host "cua-driver-rs uninstalled." -ForegroundColor Green
Write-Host ""
Write-Host "Claude Code MCP registrations:" -ForegroundColor Yellow
Write-Host " We don't auto-edit ~/.claude.json on Windows. If you registered cua-driver-rs"
Write-Host " with Claude Code, remove it manually:"
Write-Host ""
Write-Host " claude mcp remove cua-driver-rs"
Write-Host ""
Write-Host " Or edit ~/.claude.json directly and delete entries whose 'command' points at"
Write-Host " cua-driver.exe under %LOCALAPPDATA%\Programs\Cua\cua-driver\bin\"
Write-Host " (or the legacy %LOCALAPPDATA%\Programs\trycua\cua-driver-rs\bin\ from v0.2.13 and earlier)."
Write-Host ""
Write-Host "PATH:"
Write-Host " If you added $VisibleBinDir to your User PATH after the install, remove it:"
Write-Host ""
Write-Host " `$old = [Environment]::GetEnvironmentVariable('Path', 'User')"
Write-Host " `$new = ((`$old.Split(';')) | Where-Object { `$_ -and (`$_.TrimEnd('\') -ne '$($VisibleBinDir.TrimEnd('\'))') }) -join ';'"
Write-Host " [Environment]::SetEnvironmentVariable('Path', `$new, 'User')"
Write-Host ""
Write-Host " Then open a new PowerShell window for the change to take effect."
Write-Host ""