install/uninstall.ps1
Rama master · réplica de lectura del repositorio original.
param(
[string]$InstallDir = "",
[string]$ServiceName = "MetisAI",
[switch]$KeepData,
[switch]$RemoveData,
[switch]$DryRun,
[switch]$Yes
)
$ErrorActionPreference = "Stop"
if (-not $InstallDir) {
try {
$InstallDir = (Get-ItemProperty -LiteralPath "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\$ServiceName" -ErrorAction Stop).InstallLocation
} catch {}
}
if (-not $InstallDir) {
$runKey = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run"
foreach ($candidate in @($ServiceName, "MetisAI", "metis-ai")) {
try {
$existingRun = (Get-ItemProperty -LiteralPath $runKey -Name "$candidate-app" -ErrorAction Stop)."$candidate-app"
if ($existingRun) {
$cmdPath = [string]$existingRun.Trim().Trim('"')
if (Test-Path -LiteralPath $cmdPath) {
$InstallDir = Split-Path -Parent $cmdPath
$ServiceName = $candidate
break
}
}
} catch {}
}
}
if (-not $InstallDir) { throw "Metis AI is not installed (no startup entry for $ServiceName). Nothing to uninstall. Pass -InstallDir DIR if files are leftover." }
$manifestPath = Join-Path $InstallDir ".metis-ai-install.json"
$manifest = $null
if (Test-Path $manifestPath) {
$manifest = Get-Content $manifestPath -Raw | ConvertFrom-Json
} else {
$manifest = [pscustomobject]@{ serviceName = $ServiceName; dataDir = ""; installMethod = "native" }
}
if ([IO.Path]::GetFullPath($InstallDir).TrimEnd("\") -eq [IO.Path]::GetPathRoot([IO.Path]::GetFullPath($InstallDir)).TrimEnd("\")) {
throw "Refusing to remove a filesystem root."
}
if (-not $Yes) {
$answer = Read-Host "Remove Metis AI installation at $InstallDir? Type 'yes'"
if ($answer -cne "yes") { Write-Host "Aborted."; exit 0 }
}
function Invoke-Step([scriptblock]$Action, [string]$Description) {
if ($DryRun) { Write-Host "+ $Description" } else { & $Action }
}
function Test-InstallProcess($Process, [string[]]$Roots) {
if ($Process.Name -notin @("node.exe", "esbuild.exe")) { return $false }
foreach ($root in $Roots) {
if (-not $root) { continue }
$prefix = $root.TrimEnd('\') + '\'
if ($Process.Name -eq "node.exe" -and $Process.CommandLine -and
$Process.CommandLine.IndexOf($prefix, [StringComparison]::OrdinalIgnoreCase) -ge 0) { return $true }
if ($Process.Name -eq "esbuild.exe" -and $Process.ExecutablePath -and
$Process.ExecutablePath.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase)) { return $true }
}
return $false
}
$runKey = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run"
$rootNorm = [IO.Path]::GetFullPath($InstallDir).TrimEnd("\")
$hostExe = Join-Path $InstallDir "MetisHost.exe"
$scriptHost = Join-Path $InstallDir "windows-script-host.mjs"
if (Test-Path -LiteralPath $scriptHost) {
Invoke-Step {
$node = (Get-Command node -ErrorAction Stop).Source
& $node $scriptHost --stop
if ($LASTEXITCODE -ne 0) { throw "Metis script host did not stop. Check host.log." }
} "Stop Metis script background host"
}
$hostProcesses = @(Get-CimInstance Win32_Process -Filter "Name='MetisHost.exe'" -ErrorAction SilentlyContinue |
Where-Object { $_.ExecutablePath -and [IO.Path]::GetFullPath($_.ExecutablePath) -eq [IO.Path]::GetFullPath($hostExe) })
if ($hostProcesses.Count) {
Invoke-Step {
Start-Process -FilePath $hostExe -ArgumentList "--stop" -Wait
$deadline = [DateTime]::UtcNow.AddSeconds(15)
do {
$running = @(Get-CimInstance Win32_Process -Filter "Name='MetisHost.exe'" -ErrorAction SilentlyContinue |
Where-Object { $_.ExecutablePath -and [IO.Path]::GetFullPath($_.ExecutablePath) -eq [IO.Path]::GetFullPath($hostExe) })
if (-not $running.Count) { break }
Start-Sleep -Milliseconds 250
} while ([DateTime]::UtcNow -lt $deadline)
foreach ($proc in $running) {
Stop-Process -Id $proc.ProcessId -Force -ErrorAction SilentlyContinue
}
if (@(Get-CimInstance Win32_Process -Filter "Name='MetisHost.exe'" -ErrorAction SilentlyContinue |
Where-Object { $_.ExecutablePath -and [IO.Path]::GetFullPath($_.ExecutablePath) -eq [IO.Path]::GetFullPath($hostExe) }).Count) {
throw "Metis host did not stop. Check host.log."
}
} "Stop Metis background host"
}
Invoke-Step { Remove-ItemProperty -LiteralPath $runKey -Name "$($manifest.serviceName)-host" -ErrorAction SilentlyContinue } "Remove host startup entry"
Invoke-Step { Remove-Item -LiteralPath (Join-Path ([Environment]::GetFolderPath("Programs")) $manifest.serviceName) -Recurse -Force -ErrorAction SilentlyContinue } "Remove Metis Start menu entry"
Invoke-Step { Remove-Item -LiteralPath "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\$($manifest.serviceName)" -Recurse -Force -ErrorAction SilentlyContinue } "Remove Metis application registration"
if ($manifest.installMethod -eq "docker") {
Invoke-Step { Push-Location $InstallDir; docker compose down; Pop-Location } "docker compose down"
} else {
foreach ($suffix in @("app", "worker", "mcp")) {
$task = "$($manifest.serviceName)-$suffix"
Invoke-Step { Remove-ItemProperty -LiteralPath $runKey -Name $task -ErrorAction SilentlyContinue } "Remove startup entry $task"
Invoke-Step { cmd.exe /c "schtasks /Delete /TN `"$task`" /F >nul 2>&1" } "Delete scheduled task $task"
}
$rootAliases = @($InstallDir, $rootNorm)
if ($manifest.installDir -and [IO.Path]::GetFullPath([string]$manifest.installDir).TrimEnd('\') -eq $rootNorm) {
$rootAliases += [string]$manifest.installDir
}
Invoke-Step {
Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
Where-Object { Test-InstallProcess $_ $rootAliases } |
ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
} "Stop running Metis node processes"
}
function Remove-Tree([string]$Path) {
if (-not (Test-Path -LiteralPath $Path)) { return }
$target = $Path
if ($target -notlike "\\?\*") { $target = "\\?\$Path" }
for ($i = 0; $i -lt 8; $i++) {
cmd.exe /c "rmdir /s /q `"$target`"" | Out-Null
if (-not (Test-Path -LiteralPath $Path)) { return }
Start-Sleep -Seconds 2
}
throw "Failed to remove $Path"
}
$shouldKeepData = $KeepData -and -not $RemoveData
$keepStash = $null
$keepEnvStash = $null
if ($shouldKeepData) {
$envFile = Join-Path $InstallDir ".env"
if (Test-Path -LiteralPath $envFile) {
$keepEnvStash = Join-Path (Split-Path -Parent $rootNorm) (".$(Split-Path -Leaf $rootNorm).metis-keep-env")
Invoke-Step {
Copy-Item -LiteralPath $envFile -Destination $keepEnvStash -Force
} "Stash .env to $keepEnvStash"
}
$dataDir = [string]$manifest.dataDir
if (-not $dataDir) {
$envFile = Join-Path $InstallDir ".env"
if (Test-Path -LiteralPath $envFile) {
$line = Get-Content -LiteralPath $envFile | Where-Object { $_ -match '^CHAT_DATA_DIR=' } | Select-Object -First 1
if ($line) { $dataDir = $line.Substring('CHAT_DATA_DIR='.Length).Trim().Trim('"') }
}
}
if (-not $dataDir -and (Test-Path -LiteralPath (Join-Path $InstallDir "data"))) {
$dataDir = Join-Path $InstallDir "data"
}
if ($dataDir -and (Test-Path -LiteralPath $dataDir)) {
$dataFull = [IO.Path]::GetFullPath($dataDir).TrimEnd('\')
if ($dataFull -ne $rootNorm -and $dataFull.StartsWith($rootNorm + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) {
$keepStash = "$rootNorm.metis-keep-data"
Invoke-Step {
if (Test-Path -LiteralPath $keepStash) { Remove-Item -LiteralPath $keepStash -Recurse -Force }
Move-Item -LiteralPath $dataDir -Destination $keepStash
} "Stash nested data to $keepStash"
}
}
}
if (-not $shouldKeepData -and $manifest.dataDir -and ([IO.Path]::GetFullPath($manifest.dataDir) -ne [IO.Path]::GetFullPath($InstallDir))) {
Invoke-Step { Remove-Tree ([IO.Path]::GetFullPath($manifest.dataDir)) } "Remove data directory"
}
Invoke-Step { Start-Sleep -Seconds 1; Remove-Tree $rootNorm } "Remove installation directory"
if ($keepStash -or $keepEnvStash) {
Write-Host "Metis AI uninstalled. Data kept: $(if ($keepStash) { $keepStash } else { $shouldKeepData }); env kept: $(if ($keepEnvStash) { $keepEnvStash } else { 'none' })"
} else {
Write-Host "Metis AI uninstalled. Data kept: $shouldKeepData"
}