param( [switch] $VoiceNim, [switch] $VoiceLocal, [switch] $VoiceAll, [string] $TorchBackend = "", [switch] $DryRun, [switch] $Help, [Parameter(ValueFromRemainingArguments = $true)] [object[]] $RemainingArgs = @() ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" $RepoGitUrl = "git+https://github.com/Alishahryar1/free-claude-code.git" $PythonVersion = "3.14.0" $MinUvVersion = "0.11.0" $UvInstallUrl = "https://astral.sh/uv/install.ps1" function Show-Usage { @" Usage: install.ps1 [options] Installs Claude Code if missing, installs or updates uv, Python 3.14.0, and Free Claude Code. Options: -VoiceNim Install NVIDIA NIM voice transcription support. -VoiceLocal Install local Whisper voice transcription support. -VoiceAll Install all voice transcription backends. -TorchBackend VALUE Use a uv PyTorch backend, such as cu130. Requires local voice. -DryRun Print commands without running them. -Help Show this help text. "@ } function Write-Step { param([string] $Message) Write-Host "" Write-Host "==> $Message" } function Format-Argument { param([string] $Value) if ($Value -match '^[A-Za-z0-9_./:@%+=,\[\]-]+$') { return $Value } return "'" + ($Value -replace "'", "''") + "'" } function Invoke-InstallCommand { param( [string] $FilePath, [string[]] $Arguments = @() ) $parts = @($FilePath) + $Arguments $commandText = ($parts | ForEach-Object { Format-Argument ([string] $_) }) -join " " Write-Host "+ $commandText" if (-not $DryRun) { & $FilePath @Arguments } } function Invoke-UvInstaller { Write-Host "+ irm $UvInstallUrl | iex" if (-not $DryRun) { Invoke-RestMethod $UvInstallUrl | Invoke-Expression } } function Add-PathEntry { param([string] $PathEntry) if ([string]::IsNullOrWhiteSpace($PathEntry)) { return } $separator = [IO.Path]::PathSeparator $entries = @() if (-not [string]::IsNullOrEmpty($env:Path)) { $entries = $env:Path -split [regex]::Escape([string] $separator) } if ($entries -notcontains $PathEntry) { $env:Path = "$PathEntry$separator$env:Path" } } function Add-UvToPath { Add-PathEntry (Join-Path $HOME ".local\bin") Add-PathEntry (Join-Path $HOME ".cargo\bin") } function Assert-CommandAvailable { param([string] $Name) if ((-not $DryRun) -and (-not (Get-Command $Name -ErrorAction SilentlyContinue))) { throw "$Name is required. Install it first, then rerun this installer." } } function Invoke-ProbeCommand { param( [string] $FilePath, [string[]] $Arguments = @() ) $previousErrorActionPreference = $ErrorActionPreference $ErrorActionPreference = "Continue" try { $output = & $FilePath @Arguments 2>$null return [pscustomobject] @{ ExitCode = $LASTEXITCODE Output = ($output | Out-String) } } catch { return [pscustomobject] @{ ExitCode = 1 Output = "" } } finally { $ErrorActionPreference = $previousErrorActionPreference } } function Get-InstalledUvVersion { $version = "" $selfVersionProbe = Invoke-ProbeCommand -FilePath "uv" -Arguments @("self", "version", "--short") if ($selfVersionProbe.ExitCode -eq 0) { $version = $selfVersionProbe.Output.Trim() } if ([string]::IsNullOrWhiteSpace($version)) { $versionProbe = Invoke-ProbeCommand -FilePath "uv" -Arguments @("--version") if (($versionProbe.ExitCode -eq 0) -and ($versionProbe.Output -match '^uv\s+([^\s]+)')) { $version = $Matches[1] } } if ([string]::IsNullOrWhiteSpace($version)) { throw "Unable to determine uv version." } return $version } function Test-UvVersionAtLeast { param( [string] $Version, [string] $Minimum ) $normalizedVersion = $Version -replace '[-+].*$', '' $normalizedMinimum = $Minimum -replace '[-+].*$', '' return ([version] $normalizedVersion) -ge ([version] $normalizedMinimum) } function Test-UvVersionSatisfiesMinimum { $version = Get-InstalledUvVersion return Test-UvVersionAtLeast -Version $version -Minimum $MinUvVersion } function Assert-MinUvVersion { if ($DryRun) { return } $version = Get-InstalledUvVersion if (-not (Test-UvVersionAtLeast -Version $version -Minimum $MinUvVersion)) { throw "uv $MinUvVersion or newer is required; found uv $version. Upgrade uv with its installer or package manager, then rerun this installer." } } function Test-UvSelfUpdateSupported { $probe = Invoke-ProbeCommand -FilePath "uv" -Arguments @("self", "update", "--dry-run") return $probe.ExitCode -eq 0 } function Test-UvInstalledByScoop { if (-not (Get-Command scoop -ErrorAction SilentlyContinue)) { return $false } $probe = Invoke-ProbeCommand -FilePath "scoop" -Arguments @("list", "uv") return ($probe.ExitCode -eq 0) -and ($probe.Output -match '(^|\s)uv(\s|$)') } function Test-UvInstalledByWinget { if (-not (Get-Command winget -ErrorAction SilentlyContinue)) { return $false } $probe = Invoke-ProbeCommand -FilePath "winget" -Arguments @("list", "--id", "astral-sh.uv", "-e") return ($probe.ExitCode -eq 0) -and ($probe.Output -match 'astral-sh\.uv') } function Test-UvInstalledByPipx { if (-not (Get-Command pipx -ErrorAction SilentlyContinue)) { return $false } $probe = Invoke-ProbeCommand -FilePath "pipx" -Arguments @("list") return ($probe.ExitCode -eq 0) -and ($probe.Output -match '(?m)\bpackage uv\b') } function Test-UvInstalledInActiveVirtualenv { if ([string]::IsNullOrWhiteSpace($env:VIRTUAL_ENV)) { return $false } $uvCommand = Get-Command uv -ErrorAction SilentlyContinue if (-not $uvCommand) { return $false } $uvPath = [IO.Path]::GetFullPath($uvCommand.Source) $venvPath = ([IO.Path]::GetFullPath($env:VIRTUAL_ENV)).TrimEnd( [IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar ) $nativePrefix = "$venvPath$([IO.Path]::DirectorySeparatorChar)" $alternatePrefix = "$venvPath$([IO.Path]::AltDirectorySeparatorChar)" return $uvPath.StartsWith($nativePrefix, [StringComparison]::OrdinalIgnoreCase) -or $uvPath.StartsWith($alternatePrefix, [StringComparison]::OrdinalIgnoreCase) } function Update-ExistingUv { if (Test-UvSelfUpdateSupported) { Invoke-InstallCommand -FilePath "uv" -Arguments @("self", "update") return } if (Test-UvInstalledByScoop) { Invoke-InstallCommand -FilePath "scoop" -Arguments @("update", "uv") return } if (Test-UvInstalledByWinget) { Invoke-InstallCommand -FilePath "winget" -Arguments @( "upgrade", "--id", "astral-sh.uv", "-e", "--accept-package-agreements", "--accept-source-agreements" ) return } if (Test-UvInstalledByPipx) { Invoke-InstallCommand -FilePath "pipx" -Arguments @("upgrade", "uv") return } if (Test-UvInstalledInActiveVirtualenv) { Invoke-InstallCommand -FilePath "python" -Arguments @("-m", "pip", "install", "--upgrade", "uv") return } if (Test-UvVersionSatisfiesMinimum) { Write-Host "uv is already installed and satisfies >=$MinUvVersion; skipping automatic uv update because the install source was not detected." return } $version = "unknown" try { $version = Get-InstalledUvVersion } catch { $version = "unknown" } throw "uv $MinUvVersion or newer is required; found uv $version. The existing uv install source was not detected. Upgrade uv manually with the package manager that installed it, then rerun this installer." } function Install-ClaudeIfMissing { if (Get-Command claude -ErrorAction SilentlyContinue) { Write-Host "Claude Code already found on PATH; skipping install." return } Assert-CommandAvailable "npm" Invoke-InstallCommand -FilePath "npm" -Arguments @("install", "-g", "@anthropic-ai/claude-code") } function Install-OrUpdateUv { Add-UvToPath if (Get-Command uv -ErrorAction SilentlyContinue) { Update-ExistingUv Assert-MinUvVersion return } Invoke-UvInstaller Add-UvToPath if ((-not $DryRun) -and (-not (Get-Command uv -ErrorAction SilentlyContinue))) { throw "uv was installed, but it is not available on PATH. Open a new terminal or add uv's bin directory to PATH." } Assert-MinUvVersion } function Get-PackageSpec { $includeNim = $VoiceNim $includeLocal = $VoiceLocal if ($VoiceAll) { $includeNim = $true $includeLocal = $true } if ((-not [string]::IsNullOrWhiteSpace($TorchBackend)) -and (-not $includeLocal)) { throw "-TorchBackend requires -VoiceLocal or -VoiceAll." } if ($includeNim -and $includeLocal) { return "free-claude-code[voice,voice_local] @ $RepoGitUrl" } if ($includeNim) { return "free-claude-code[voice] @ $RepoGitUrl" } if ($includeLocal) { return "free-claude-code[voice_local] @ $RepoGitUrl" } return $RepoGitUrl } function Install-FreeClaudeCode { $packageSpec = Get-PackageSpec $toolArgs = @("tool", "install", "--force") if (-not [string]::IsNullOrWhiteSpace($TorchBackend)) { $toolArgs += @("--torch-backend", $TorchBackend) } $toolArgs += $packageSpec Invoke-InstallCommand -FilePath "uv" -Arguments $toolArgs } if ($Help) { Show-Usage return } if ($RemainingArgs.Count -gt 0) { Show-Usage throw "Unknown option: $($RemainingArgs -join ' ')" } if ((-not [string]::IsNullOrWhiteSpace($TorchBackend)) -and (-not ($VoiceLocal -or $VoiceAll))) { throw "-TorchBackend requires -VoiceLocal or -VoiceAll." } Write-Step "Installing Claude Code if missing" Install-ClaudeIfMissing Write-Step "Installing uv if missing, updating if present" Install-OrUpdateUv Write-Step "Installing Python $PythonVersion" Invoke-InstallCommand -FilePath "uv" -Arguments @("python", "install", $PythonVersion) Write-Step "Installing or updating Free Claude Code" Install-FreeClaudeCode Write-Host "" Write-Host "Free Claude Code is installed. Start the proxy with: fcc-server"