# phoneide official Windows installer (PowerShell) # Usage: # irm https://phoneide.samai.cc/install.ps1 | iex # irm https://phoneide.samai.cc/install.ps1 | iex -Version 1.2.11 # # Installs PhoneIDE as a standalone .exe (no Python required). # # [FIX 2026-06-30] This installer used to silently coexist with a previously # `pip install phoneide`-ed copy: the pip-created `phoneide.exe` lives in the # Python Scripts dir and is typically EARLIER on PATH than $InstallDir, so # typing `phoneide` invoked the pip version which spins up the in-process # Flask server AND the system tray at the same time ! which on machines that # also had `pip install teambot-ai` looked like "phoneide starts teambot" # (two tray icons, two browser windows, two background processes). # # Fixes: # 1. Pre-install: detect & uninstall any pip-installed `phoneide` package # from every Python on the system, so only the standalone exe remains. # 2. Pre-install: detect any running phoneide.exe / phoneide_server.py / # teambot.exe / teambot tray processes and offer to kill them, so we do # not overwrite a running binary (Windows file lock) and do not leave # stale tray icons. # 3. PATH: prepend $InstallDir to the FRONT of user PATH (not append), # so our standalone exe always wins over any leftover Python Scripts # dir that might still contain a pip-created phoneide.exe stub. # 4. Post-install: verify `phoneide` resolves to $Target (Get-Command); # warn loudly if it does not (means another phoneide.exe is shadowing). [CmdletBinding()] param( [string]$Version = "" ) $ErrorActionPreference = "Stop" # ---- defaults ------------------------------------------------------------- $BaseUrl = "https://phoneide.samai.cc/downloads" $BinName = "phoneide.exe" # ---- helpers -------------------------------------------------------------- function Write-Info { param($msg) Write-Host "[OK] $msg" -ForegroundColor Cyan } function Write-Warn { param($msg) Write-Host "[!] $msg" -ForegroundColor Yellow } function Write-Err { param($msg) Write-Host "[X] $msg" -ForegroundColor Red } function Die { param($msg) Write-Err $msg; exit 1 } # ---- detect arch ---------------------------------------------------------- $PlatformArch = "amd64" if ($env:PROCESSOR_ARCHITECTURE -match "ARM64") { $PlatformArch = "arm64" } $RemoteFile = "phoneide-windows-$PlatformArch.exe" Write-Info "Detected platform: windows-$PlatformArch" # ---- pick install dir ----------------------------------------------------- $IsAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if ($IsAdmin) { $InstallDir = "C:\Program Files\phoneide" $Target = Join-Path $InstallDir $BinName } else { $InstallDir = "$env:LOCALAPPDATA\phoneide" $Target = Join-Path $InstallDir $BinName } if (-not (Test-Path $InstallDir)) { New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null } # ---- [FIX 2026-06-30] pre-install: kill stale processes ------------------- # A running phoneide.exe holds a file lock on $Target that prevents Copy-Item # from replacing it (Copy-Item silently writes a partial file or errors out). # A running phoneide_server.py / teambot tray leaves a stale tray icon that # users perceive as "phoneide started teambot too". $ProcessNamesToKill = @("phoneide", "phoneide_server") # We do NOT kill teambot.exe here ! that's the user's call. We just warn. foreach ($pname in $ProcessNamesToKill) { $procs = Get-Process -Name $pname -ErrorAction SilentlyContinue if ($procs) { foreach ($p in $procs) { try { $pPath = $p.Path } catch { $pPath = "" } Write-Warn "Killing stale process $($p.Name) (PID $($p.Id)) -> $pPath" try { $p | Stop-Process -Force -ErrorAction SilentlyContinue } catch {} } Start-Sleep -Milliseconds 800 } } # Also detect (but DO NOT kill) teambot.exe ! warn the user. This is the # source of the "phoneide started teambot too" confusion: teambot tray was # already running before phoneide started, the user just noticed it now. $teambotProcs = Get-Process -Name "teambot" -ErrorAction SilentlyContinue if ($teambotProcs) { Write-Warn "Detected teambot.exe running (PID $($teambotProcs.Id -join ', '))." Write-Warn " This is NOT started by phoneide. It is a separate install." Write-Warn " If you do not want teambot, uninstall it via:" Write-Warn " pip uninstall teambot-ai" Write-Warn " (or remove $env:LOCALAPPDATA\teambot\teambot.exe)" } # ---- [FIX 2026-06-30] pre-install: uninstall pip-installed phoneide ------- # Find every python.exe on the system and try `pip uninstall -y phoneide`. # This removes the `phoneide.exe` stub from each Python's Scripts dir, which # otherwise shadows our standalone exe on PATH. $PythonCandidates = @() $defaultPy = Get-Command python -ErrorAction SilentlyContinue if ($defaultPy) { $PythonCandidates += $defaultPy.Source } $py3 = Get-Command python3 -ErrorAction SilentlyContinue if ($py3 -and $py3.Source -notin $PythonCandidates) { $PythonCandidates += $py3.Source } # Standard winget/python.org install locations $localAppPy = Join-Path $env:LOCALAPPDATA "Programs\Python" if (Test-Path $localAppPy) { Get-ChildItem -Path $localAppPy -Filter "python.exe" -Recurse -Depth 1 -ErrorAction SilentlyContinue | ForEach-Object { if ($_.FullName -notin $PythonCandidates) { $PythonCandidates += $_.FullName } } } foreach ($py in $PythonCandidates) { if (-not (Test-Path $py)) { continue } try { # Check if phoneide is installed in THIS interpreter $checkOut = & $py -c "import phoneide_server; print('INSTALLED')" 2>$null if ($LASTEXITCODE -eq 0 -and $checkOut -match "INSTALLED") { Write-Warn "Found pip-installed phoneide in $py ! uninstalling to avoid shadowing standalone exe..." & $py -m pip uninstall -y phoneide 2>&1 | Out-Null if ($LASTEXITCODE -eq 0) { Write-Info "Uninstalled pip phoneide from $py" } else { Write-Warn "Could not uninstall pip phoneide from $py (rc=$LASTEXITCODE)" } } } catch { # Interpreter is broken or pip missing ! skip silently } } # ---- pick version --------------------------------------------------------- if (-not $Version) { Write-Info "Fetching latest version from $BaseUrl/latest-version.txt ..." try { $Version = (Invoke-RestMethod -Uri "$BaseUrl/latest-version.txt" -UseBasicParsing).Trim() } catch { Die "Could not determine latest version: $_" } if (-not $Version) { Die "Could not determine latest version." } } Write-Info "Installing phoneide v$Version" # ---- download ------------------------------------------------------------- $TmpFile = [System.IO.Path]::GetTempFileName() $Url = "$BaseUrl/$RemoteFile" Write-Info "Downloading $Url ..." try { Invoke-WebRequest -Uri $Url -OutFile $TmpFile -UseBasicParsing } catch { Die "Download failed: $_" } # ---- verify SHA256 -------------------------------------------------------- Write-Info "Verifying SHA256 checksum ..." try { $ChecksumsContent = Invoke-RestMethod -Uri "$BaseUrl/checksums-sha256.txt" -UseBasicParsing $ExpectedHash = ($ChecksumsContent -split "`n" | Where-Object { $_ -match "\s+$RemoteFile$" } | ForEach-Object { ($_ -split "\s+")[0] } | Select-Object -First 1) } catch { $ExpectedHash = $null } if (-not $ExpectedHash) { Write-Warn "No checksum entry found for $RemoteFile - skipping verification." } else { $ActualHash = (Get-FileHash -Path $TmpFile -Algorithm SHA256).Hash.ToLower() if ($ExpectedHash.ToLower() -ne $ActualHash) { Die "Checksum mismatch!`n expected: $ExpectedHash`n actual: $ActualHash" } Write-Info "Checksum OK" } # ---- install -------------------------------------------------------------- Write-Info "Installing to $Target ..." Copy-Item -Path $TmpFile -Destination $Target -Force Remove-Item -Path $TmpFile -Force # ---- [FIX 2026-06-30] PATH management: PREPEND, not append ---------------- # A pip-installed phoneide creates `phoneide.exe` in the Python Scripts dir. # That dir is typically EARLIER on PATH than our $InstallDir, so `phoneide` # would invoke the pip version (which spawns Flask + tray in-process, the # exact behavior users mistake for "phoneide started teambot"). # # Fix: put $InstallDir at the FRONT of user PATH so our standalone exe wins. $UserPath = [Environment]::GetEnvironmentVariable("Path", "User") if (-not $UserPath) { $UserPath = "" } # Split into entries, drop empty ones and existing $InstallDir entries $entries = $UserPath -split ';' | Where-Object { $_ -and $_ -ne $InstallDir } # Prepend our dir $entries = @($InstallDir) + $entries $newUserPath = ($entries -join ';') [Environment]::SetEnvironmentVariable("Path", $newUserPath, "User") # Also update current session PATH, again prepending $env:Path = "$InstallDir;$env:Path" Write-Info "$InstallDir prepended to user PATH (takes effect in new terminals)" # ---- [FIX 2026-06-30] verify Get-Command actually resolves to $Target ------ $resolved = $null try { $cmd = Get-Command phoneide -ErrorAction Stop if ($cmd) { $resolved = $cmd.Source } } catch {} if ($resolved -and ($resolved -ieq $Target)) { Write-Info "Verified: 'phoneide' resolves to $Target" } elseif ($resolved) { Write-Warn "'phoneide' currently resolves to: $resolved" Write-Warn " This is NOT the newly-installed standalone exe ($Target)." Write-Warn " Open a NEW terminal so the updated PATH takes effect, or remove the shadowing entry." } else { Write-Warn "Could not resolve 'phoneide' on current PATH (will work in a new terminal)." } # ---- verify --------------------------------------------------------------- Write-Info "Verifying installation ..." if (Test-Path $Target) { $size = (Get-Item $Target).Length / 1MB Write-Info "phoneide v$Version installed to $Target ($([math]::Round($size, 1)) MB)" } else { Die "Binary not found at $Target" } Write-Host "" Write-Info "Done! Try it now (open a NEW terminal first):" Write-Host " phoneide # start PhoneIDE (default port 12345)" Write-Host " phoneide --port 8080 # start on a custom port" Write-Host "" Write-Info "Docs: https://phoneide.samai.cc | Releases: https://phoneide.samai.cc/downloads/"