| param( |
| [switch] $VoiceNim, |
| [switch] $VoiceLocal, |
| [switch] $VoiceAll, |
| [string] $TorchBackend = "", |
| [switch] $DryRun, |
| [switch] $Help, |
| [Parameter(ValueFromRemainingArguments = $true)] |
| [object[]] $RemainingArgs = @() |
| ) |
|
|
| Set-StrictMode -Version Latest |
| $ErrorActionPreference = "Stop" |
| $ProgressPreference = "SilentlyContinue" |
|
|
| $RepoArchiveUrl = "https://github.com/Alishahryar1/free-claude-code/archive/refs/heads/main.zip" |
| |
| $PythonRequest = "cpython-3.14.0-windows-x86_64-none" |
| $MinUvVersion = "0.11.16" |
| $ClaudeInstallUrl = "https://claude.ai/install.ps1" |
| $CodexInstallUrl = "https://chatgpt.com/codex/install.ps1" |
| $PiInstallUrl = "https://pi.dev/install.ps1" |
| $UvInstallUrl = "https://astral.sh/uv/install.ps1" |
| $script:InstallClaudeCode = $true |
| $script:InstallCodex = $true |
| $script:InstallPi = $true |
| $script:PiAvailable = $false |
| $FccCommands = @( |
| |
| "fcc-desktop", |
| "fcc-server", |
| "fcc-claude", |
| "fcc-codex", |
| "fcc-pi", |
| "fcc-init", |
| "free-claude-code" |
| ) |
|
|
| function Show-Usage { |
| @" |
| Usage: install.ps1 [options] |
| |
| Installs or updates Free Claude Code and lets you choose which coding agents to install or verify. |
| |
| 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 Test-InteractiveInstaller { |
| return (-not [Console]::IsInputRedirected) -and (-not [Console]::IsOutputRedirected) |
| } |
|
|
| function Read-YesNo { |
| param([string] $Prompt) |
|
|
| while ($true) { |
| $answer = ([string] (Read-Host "$Prompt [Y/n]")).Trim().ToLowerInvariant() |
| if ($answer -in @("", "y", "yes")) { |
| return $true |
| } |
| if ($answer -in @("n", "no")) { |
| return $false |
| } |
| Write-Host "Please answer Y or N." |
| } |
| } |
|
|
| function Select-CodingAgents { |
| while ($true) { |
| $script:InstallClaudeCode = Read-YesNo "Install or verify Claude Code for fcc-claude?" |
| $script:InstallCodex = Read-YesNo "Install or verify Codex for fcc-codex?" |
| $script:InstallPi = Read-YesNo "Install or verify Pi for fcc-pi?" |
|
|
| if ($script:InstallClaudeCode -or $script:InstallCodex -or $script:InstallPi) { |
| return |
| } |
| Write-Host "Select at least one coding agent." |
| Write-Host "" |
| } |
| } |
|
|
| function Format-Argument { |
| param([string] $Value) |
|
|
| if ($Value -match '^[A-Za-z0-9_./:@%+=,\[\]\\-]+$') { |
| return $Value |
| } |
|
|
| return "'" + ($Value -replace "'", "''") + "'" |
| } |
|
|
| function Format-Command { |
| param( |
| [string] $FilePath, |
| [string[]] $Arguments = @() |
| ) |
|
|
| $parts = @($FilePath) + $Arguments |
| return ($parts | ForEach-Object { Format-Argument ([string] $_) }) -join " " |
| } |
|
|
| function Invoke-NativeCommand { |
| param( |
| [string] $FilePath, |
| [string[]] $Arguments = @() |
| ) |
|
|
| $commandText = Format-Command -FilePath $FilePath -Arguments $Arguments |
| Write-Host "+ $commandText" |
| if ($DryRun) { |
| return |
| } |
|
|
| $global:LASTEXITCODE = 0 |
| & $FilePath @Arguments |
| $exitCode = $LASTEXITCODE |
| if ($exitCode -ne 0) { |
| throw "Command failed with exit code ${exitCode}: $commandText" |
| } |
| } |
|
|
| function Invoke-Utf8NativeCapture { |
| param( |
| [string] $FilePath, |
| [string[]] $Arguments = @() |
| ) |
|
|
| $commandText = Format-Command -FilePath $FilePath -Arguments $Arguments |
| Write-Host "+ $commandText" |
| $originalOutputEncoding = [Console]::OutputEncoding |
| try { |
| [Console]::OutputEncoding = [Text.UTF8Encoding]::new($false) |
| $global:LASTEXITCODE = 0 |
| $output = & $FilePath @Arguments |
| $exitCode = $LASTEXITCODE |
| } |
| finally { |
| [Console]::OutputEncoding = $originalOutputEncoding |
| } |
| if ($exitCode -ne 0) { |
| throw "Command failed with exit code ${exitCode}: $commandText" |
| } |
|
|
| return ($output | Out-String).Trim() |
| } |
|
|
| function Get-ApplicationCommand { |
| param([string] $Name) |
|
|
| $commands = @(Get-Command $Name -CommandType Application -ErrorAction SilentlyContinue) |
| if ($commands.Count -eq 0) { |
| return $null |
| } |
|
|
| return $commands[0] |
| } |
|
|
| function Get-PowerShellExecutable { |
| param([string] $PowerShellHome = $PSHOME) |
|
|
| $executableName = if ($PSVersionTable.PSEdition -eq "Core") { |
| "pwsh.exe" |
| } |
| else { |
| "powershell.exe" |
| } |
| $bundledExecutable = Join-Path $PowerShellHome $executableName |
| if (Test-Path -LiteralPath $bundledExecutable -PathType Leaf) { |
| return $bundledExecutable |
| } |
|
|
| $pathCommand = Get-ApplicationCommand ([IO.Path]::GetFileNameWithoutExtension($executableName)) |
| if ($pathCommand) { |
| return $pathCommand.Source |
| } |
|
|
| throw "Unable to locate a PowerShell executable for the downloaded installer." |
| } |
|
|
| 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-KnownBinDirectories { |
| if (-not [string]::IsNullOrWhiteSpace($env:USERPROFILE)) { |
| Add-PathEntry (Join-Path $env:USERPROFILE ".local\bin") |
| } |
| if (-not [string]::IsNullOrWhiteSpace($env:LOCALAPPDATA)) { |
| Add-PathEntry (Join-Path $env:LOCALAPPDATA "Programs\OpenAI\Codex\bin") |
| Add-PathEntry (Join-Path $env:LOCALAPPDATA "pi-node\current") |
| } |
| if (-not [string]::IsNullOrWhiteSpace($env:APPDATA)) { |
| Add-PathEntry (Join-Path $env:APPDATA "npm") |
| } |
| } |
|
|
| function Add-PiBinDirectories { |
| if ($DryRun) { |
| return |
| } |
|
|
| Add-KnownBinDirectories |
| $npm = Get-ApplicationCommand "npm" |
| if (-not $npm) { |
| return |
| } |
|
|
| $prefix = (& $npm.Source prefix -g 2>$null | Out-String).Trim() |
| if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($prefix)) { |
| $prefix = (& $npm.Source config get prefix 2>$null | Out-String).Trim() |
| } |
| if ($LASTEXITCODE -eq 0 -and -not [string]::IsNullOrWhiteSpace($prefix)) { |
| Add-PathEntry $prefix |
| } |
| } |
|
|
| function Assert-NoFccProcessesRunning { |
| $running = @() |
| foreach ($commandName in $FccCommands) { |
| $processes = @(Get-Process -Name $commandName -ErrorAction SilentlyContinue) |
| foreach ($process in $processes) { |
| $running += "$commandName (PID $($process.Id))" |
| } |
| } |
|
|
| if ($running.Count -gt 0) { |
| throw "Free Claude Code is still running ($($running -join ', ')). Stop those processes, then rerun the installer." |
| } |
| } |
|
|
| function Invoke-DownloadedPowerShellInstaller { |
| param( |
| [string] $Url, |
| [string] $Name, |
| [switch] $NonInteractive |
| ) |
|
|
| if ($DryRun) { |
| Write-Host "+ irm $Url -OutFile <temporary-script>" |
| $prefix = if ($NonInteractive) { "CODEX_NON_INTERACTIVE=1 " } else { "" } |
| Write-Host "+ ${prefix}powershell -NoProfile -ExecutionPolicy Bypass -File <temporary-script>" |
| return |
| } |
|
|
| $temporaryScript = Join-Path ([IO.Path]::GetTempPath()) ("fcc-install-" + [guid]::NewGuid().ToString("N") + ".ps1") |
| try { |
| Write-Host "+ irm $Url -OutFile $(Format-Argument $temporaryScript)" |
| Invoke-RestMethod -Uri $Url -OutFile $temporaryScript -ErrorAction Stop |
| if ((-not (Test-Path -LiteralPath $temporaryScript)) -or ((Get-Item -LiteralPath $temporaryScript).Length -eq 0)) { |
| throw "The downloaded $Name installer was empty." |
| } |
|
|
| $tokens = $null |
| $parseErrors = $null |
| [System.Management.Automation.Language.Parser]::ParseFile( |
| $temporaryScript, |
| [ref] $tokens, |
| [ref] $parseErrors |
| ) | Out-Null |
| if ($parseErrors.Count -gt 0) { |
| throw "The downloaded $Name installer from '$Url' is not valid PowerShell. A network proxy or filter may have replaced it with an HTML response." |
| } |
|
|
| $powerShellPath = Get-PowerShellExecutable |
|
|
| $hadNonInteractive = Test-Path Env:CODEX_NON_INTERACTIVE |
| $previousNonInteractive = $env:CODEX_NON_INTERACTIVE |
| try { |
| if ($NonInteractive) { |
| $env:CODEX_NON_INTERACTIVE = "1" |
| } |
| Invoke-NativeCommand -FilePath $powerShellPath -Arguments @( |
| "-NoProfile", |
| "-ExecutionPolicy", |
| "Bypass", |
| "-File", |
| $temporaryScript |
| ) |
| } |
| finally { |
| if ($hadNonInteractive) { |
| $env:CODEX_NON_INTERACTIVE = $previousNonInteractive |
| } |
| else { |
| Remove-Item Env:CODEX_NON_INTERACTIVE -ErrorAction SilentlyContinue |
| } |
| } |
| } |
| finally { |
| Remove-Item -LiteralPath $temporaryScript -Force -ErrorAction SilentlyContinue |
| } |
| } |
|
|
| function Confirm-Application { |
| param( |
| [string] $CommandName, |
| [string] $DisplayName |
| ) |
|
|
| if ($DryRun) { |
| Write-Host "+ $CommandName --version" |
| return |
| } |
|
|
| $command = Get-ApplicationCommand $CommandName |
| if (-not $command) { |
| throw "$DisplayName was installed, but '$CommandName' is not available on PATH." |
| } |
| Invoke-NativeCommand -FilePath $command.Source -Arguments @("--version") |
| } |
|
|
| function Test-PiApplication { |
| param($Command) |
|
|
| try { |
| $helpOutput = (& $Command.Source --help 2>$null | Out-String) |
| } |
| catch { |
| return $false |
| } |
| return ( |
| $LASTEXITCODE -eq 0 -and |
| $helpOutput.Contains("--extension") -and |
| $helpOutput.Contains("--models") |
| ) |
| } |
|
|
| function Confirm-PiApplication { |
| if ($DryRun) { |
| Write-Host "+ pi --help (verify --extension and --models support)" |
| Write-Host "+ pi --version" |
| return |
| } |
|
|
| $command = Get-ApplicationCommand "pi" |
| if (-not $command) { |
| throw "Pi was installed, but 'pi' is not available on PATH." |
| } |
| if (-not (Test-PiApplication $command)) { |
| throw "The 'pi' command at '$($command.Source)' is not a compatible Pi Coding Agent." |
| } |
| Invoke-NativeCommand -FilePath $command.Source -Arguments @("--version") |
| } |
|
|
| function Ensure-ClaudeCode { |
| if (Get-ApplicationCommand "claude") { |
| Write-Host "Claude Code already found on PATH; verifying it." |
| } |
| else { |
| Invoke-DownloadedPowerShellInstaller -Url $ClaudeInstallUrl -Name "Claude Code" |
| Add-KnownBinDirectories |
| } |
|
|
| Confirm-Application -CommandName "claude" -DisplayName "Claude Code" |
| } |
|
|
| function Ensure-Codex { |
| if (Get-ApplicationCommand "codex") { |
| Write-Host "Codex already found on PATH; verifying it." |
| } |
| else { |
| Invoke-DownloadedPowerShellInstaller -Url $CodexInstallUrl -Name "Codex" -NonInteractive |
| Add-KnownBinDirectories |
| } |
|
|
| Confirm-Application -CommandName "codex" -DisplayName "Codex" |
| } |
|
|
| function Ensure-Pi { |
| $script:PiAvailable = $false |
| Add-PiBinDirectories |
| $existingPi = Get-ApplicationCommand "pi" |
| if ($existingPi -and ($DryRun -or (Test-PiApplication $existingPi))) { |
| Write-Host "Pi already found on PATH; verifying it." |
| } |
| else { |
| if ($existingPi) { |
| Write-Host "The existing 'pi' command at '$($existingPi.Source)' is not Pi Coding Agent; installing Pi." |
| } |
| Invoke-DownloadedPowerShellInstaller -Url $PiInstallUrl -Name "Pi" |
| Add-PiBinDirectories |
|
|
| if (-not $DryRun) { |
| $currentPi = Get-ApplicationCommand "pi" |
| $unchangedIncompatiblePi = ( |
| $currentPi -and |
| $existingPi -and |
| $currentPi.Source -eq $existingPi.Source -and |
| -not (Test-PiApplication $currentPi) |
| ) |
| if ((-not $currentPi) -or $unchangedIncompatiblePi) { |
| Write-Host "Pi was not installed; continuing without it." |
| return |
| } |
| } |
| } |
|
|
| Confirm-PiApplication |
| $script:PiAvailable = $true |
| } |
|
|
| function Ensure-SelectedCodingAgents { |
| if ($script:InstallClaudeCode) { |
| Write-Step "Ensuring Claude Code is installed" |
| Ensure-ClaudeCode |
| } |
|
|
| if ($script:InstallCodex) { |
| Write-Step "Ensuring Codex is installed" |
| Ensure-Codex |
| } |
|
|
| if ($script:InstallPi) { |
| Write-Step "Checking or installing Pi" |
| Ensure-Pi |
| } |
|
|
| if ((-not $script:InstallClaudeCode) -and (-not $script:InstallCodex) -and (-not $script:PiAvailable)) { |
| throw "No selected coding agent was installed. Re-run the installer and choose at least one." |
| } |
| } |
|
|
| function Convert-UvVersionOutput { |
| param([string] $Output) |
|
|
| if ([string]::IsNullOrWhiteSpace($Output)) { |
| return "" |
| } |
|
|
| if ($Output -match '(?m)(?:^|\s)(?:uv\s+)?(?<version>\d+\.\d+\.\d+(?:[-+][0-9A-Za-z][0-9A-Za-z.-]*)?)\b') { |
| return $Matches["version"] |
| } |
|
|
| return "" |
| } |
|
|
| function Get-UvVersion { |
| param([string] $UvPath) |
|
|
| $output = Invoke-Utf8NativeCapture -FilePath $UvPath -Arguments @("--version") |
| $version = Convert-UvVersionOutput $output |
| if ([string]::IsNullOrWhiteSpace($version)) { |
| throw "uv is present, but 'uv --version' did not return a valid version." |
| } |
|
|
| return $version |
| } |
|
|
| function Test-SupportedUvVersion { |
| param( |
| [string] $Version, |
| [string] $Minimum |
| ) |
|
|
| $parsedVersion = Convert-UvVersionOutput $Version |
| $parsedMinimum = Convert-UvVersionOutput $Minimum |
| if ([string]::IsNullOrWhiteSpace($parsedVersion) -or [string]::IsNullOrWhiteSpace($parsedMinimum)) { |
| throw "Unable to compare uv versions." |
| } |
| if ($parsedVersion.Contains("-")) { |
| return $false |
| } |
|
|
| $normalizedVersion = $parsedVersion -replace '\+.*$', '' |
| $normalizedMinimum = $parsedMinimum -replace '\+.*$', '' |
|
|
| return ([version] $normalizedVersion) -ge ([version] $normalizedMinimum) |
| } |
|
|
| function Confirm-Uv { |
| if ($DryRun) { |
| Write-Host "+ uv --version" |
| return |
| } |
|
|
| $uvCommand = Get-ApplicationCommand "uv" |
| if (-not $uvCommand) { |
| throw "uv was installed, but it is not available on PATH." |
| } |
|
|
| $version = Get-UvVersion $uvCommand.Source |
| if (-not (Test-SupportedUvVersion -Version $version -Minimum $MinUvVersion)) { |
| throw "Stable uv $MinUvVersion or newer is required; found uv $version after installation." |
| } |
| Write-Host "Verified uv $version." |
| } |
|
|
| function Ensure-Uv { |
| if ($DryRun) { |
| if (Get-ApplicationCommand "uv") { |
| Write-Host "+ uv --version" |
| Write-Host "A compatible existing uv will be left unchanged; an obsolete one will be replaced by the standalone installer." |
| } |
| else { |
| Write-Host "uv is not installed; the current standalone uv would be installed." |
| Invoke-DownloadedPowerShellInstaller -Url $UvInstallUrl -Name "uv" |
| Confirm-Uv |
| } |
| return |
| } |
|
|
| $uvCommand = Get-ApplicationCommand "uv" |
| if ($uvCommand) { |
| $version = Get-UvVersion $uvCommand.Source |
| if (Test-SupportedUvVersion -Version $version -Minimum $MinUvVersion) { |
| Write-Host "uv $version already satisfies >=$MinUvVersion; leaving it unchanged." |
| return |
| } |
| Write-Host "uv $version does not satisfy stable >=$MinUvVersion; installing the current standalone uv." |
| } |
| else { |
| Write-Host "uv is not installed; installing the current standalone uv." |
| } |
|
|
| Invoke-DownloadedPowerShellInstaller -Url $UvInstallUrl -Name "uv" |
| Add-KnownBinDirectories |
| Confirm-Uv |
| } |
|
|
| function Get-PackageSpec { |
| $includeNim = $VoiceNim |
| $includeLocal = $VoiceLocal |
|
|
| if ($VoiceAll) { |
| $includeNim = $true |
| $includeLocal = $true |
| } |
|
|
| if ($includeNim -and $includeLocal) { |
| return "free-claude-code[voice,voice_local] @ $RepoArchiveUrl" |
| } |
| if ($includeNim) { |
| return "free-claude-code[voice] @ $RepoArchiveUrl" |
| } |
| if ($includeLocal) { |
| return "free-claude-code[voice_local] @ $RepoArchiveUrl" |
| } |
| return "free-claude-code @ $RepoArchiveUrl" |
| } |
|
|
| function Install-FreeClaudeCode { |
| Assert-NoFccProcessesRunning |
| $packageSpec = Get-PackageSpec |
| $arguments = @( |
| "tool", |
| "install", |
| "--force", |
| "--refresh-package", |
| "free-claude-code", |
| "--python", |
| $PythonRequest |
| ) |
| if (-not [string]::IsNullOrWhiteSpace($TorchBackend)) { |
| $arguments += @("--torch-backend", $TorchBackend) |
| } |
| $arguments += $packageSpec |
|
|
| $uvPath = "uv" |
| if (-not $DryRun) { |
| $uvCommand = Get-ApplicationCommand "uv" |
| if (-not $uvCommand) { |
| throw "uv is not available for the Free Claude Code installation." |
| } |
| $uvPath = $uvCommand.Source |
| } |
| Invoke-NativeCommand -FilePath $uvPath -Arguments $arguments |
| } |
|
|
| function Export-FccDesktopIcon { |
| param( |
| [string] $DesktopCommand, |
| [string] $IconPath |
| ) |
|
|
| $arguments = @("--export-icon", $IconPath) |
| $commandText = Format-Command -FilePath $DesktopCommand -Arguments $arguments |
| Write-Host "+ $commandText" |
| if ($DryRun) { |
| return |
| } |
|
|
| |
| $process = Start-Process ` |
| -FilePath $DesktopCommand ` |
| -ArgumentList @("--export-icon", ('"' + $IconPath + '"')) ` |
| -WindowStyle Hidden ` |
| -Wait ` |
| -PassThru |
| try { |
| $exitCode = $process.ExitCode |
| } |
| finally { |
| $process.Dispose() |
| } |
| if ($exitCode -ne 0) { |
| throw "Command failed with exit code ${exitCode}: $commandText" |
| } |
| if (-not (Test-Path -LiteralPath $IconPath -PathType Leaf)) { |
| throw "Free Claude Code did not export its Windows app icon to '$IconPath'." |
| } |
| } |
|
|
| function Configure-AndConfirmFreeClaudeCode { |
| $iconPath = Join-Path $env:USERPROFILE ".fcc\app-icon.ico" |
| if ($DryRun) { |
| Write-Host "+ uv tool update-shell" |
| Write-Host "+ uv tool dir --bin" |
| Write-Host "+ verify fcc-desktop, fcc-server, fcc-claude, fcc-codex, and fcc-pi in the uv tool bin directory" |
| Write-Host "+ fcc-server --version" |
| Export-FccDesktopIcon ` |
| -DesktopCommand "<uv-tool-bin>\fcc-desktop.exe" ` |
| -IconPath $iconPath |
| Install-FccDesktopShortcuts ` |
| -DesktopCommand "<uv-tool-bin>\fcc-desktop.exe" ` |
| -IconPath $iconPath |
| return |
| } |
|
|
| $uvCommand = Get-ApplicationCommand "uv" |
| if (-not $uvCommand) { |
| throw "uv is not available for PATH configuration." |
| } |
| Invoke-NativeCommand -FilePath $uvCommand.Source -Arguments @("tool", "update-shell") |
| $toolBin = Invoke-Utf8NativeCapture -FilePath $uvCommand.Source -Arguments @("tool", "dir", "--bin") |
| if ([string]::IsNullOrWhiteSpace($toolBin)) { |
| throw "uv returned an empty tool bin directory." |
| } |
|
|
| Add-PathEntry $toolBin |
| $toolBinPath = ([IO.Path]::GetFullPath($toolBin)).TrimEnd( |
| [IO.Path]::DirectorySeparatorChar, |
| [IO.Path]::AltDirectorySeparatorChar |
| ) |
| $installedCommands = @{} |
| foreach ($commandName in @("fcc-desktop", "fcc-server", "fcc-claude", "fcc-codex", "fcc-pi")) { |
| $command = Get-ApplicationCommand $commandName |
| if (-not $command) { |
| throw "Free Claude Code installation did not create '$commandName'." |
| } |
| $commandDirectory = ([IO.Path]::GetFullPath((Split-Path -Parent $command.Source))).TrimEnd( |
| [IO.Path]::DirectorySeparatorChar, |
| [IO.Path]::AltDirectorySeparatorChar |
| ) |
| if (-not $commandDirectory.Equals($toolBinPath, [StringComparison]::OrdinalIgnoreCase)) { |
| throw "'$commandName' resolved outside the uv tool bin directory: $($command.Source)" |
| } |
| $installedCommands[$commandName] = $command.Source |
| } |
|
|
| Invoke-NativeCommand -FilePath $installedCommands["fcc-server"] -Arguments @("--version") |
| Export-FccDesktopIcon ` |
| -DesktopCommand $installedCommands["fcc-desktop"] ` |
| -IconPath $iconPath |
| Install-FccDesktopShortcuts ` |
| -DesktopCommand $installedCommands["fcc-desktop"] ` |
| -IconPath $iconPath |
| } |
|
|
| function Test-EquivalentPath { |
| param( |
| [string] $Left, |
| [string] $Right |
| ) |
|
|
| if ([string]::IsNullOrWhiteSpace($Left) -or [string]::IsNullOrWhiteSpace($Right)) { |
| return $false |
| } |
| try { |
| return [string]::Equals( |
| [IO.Path]::GetFullPath($Left), |
| [IO.Path]::GetFullPath($Right), |
| [StringComparison]::OrdinalIgnoreCase |
| ) |
| } |
| catch { |
| return $false |
| } |
| } |
|
|
| function Install-FccDesktopShortcuts { |
| param( |
| [string] $DesktopCommand, |
| [string] $IconPath |
| ) |
|
|
| $shortcutPaths = @( |
| (Join-Path $env:USERPROFILE "Desktop\Free Claude Code.lnk"), |
| (Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs\Free Claude Code.lnk") |
| ) |
| foreach ($shortcutPath in $shortcutPaths) { |
| Write-Host "+ create shortcut $(Format-Argument $shortcutPath) -> $(Format-Argument $DesktopCommand)" |
| } |
| if ($DryRun) { |
| return |
| } |
|
|
| $shell = New-Object -ComObject WScript.Shell |
| foreach ($shortcutPath in $shortcutPaths) { |
| if (Test-Path -LiteralPath $shortcutPath) { |
| try { |
| $existingShortcut = $shell.CreateShortcut($shortcutPath) |
| $isFccShortcut = Test-EquivalentPath -Left $existingShortcut.TargetPath -Right $DesktopCommand |
| } |
| catch { |
| $isFccShortcut = $false |
| } |
| if (-not $isFccShortcut) { |
| Write-Host "A shortcut not managed by Free Claude Code already exists at $shortcutPath; leaving it unchanged." |
| continue |
| } |
| } |
| $parent = Split-Path -Parent $shortcutPath |
| New-Item -ItemType Directory -Force -Path $parent | Out-Null |
| $shortcut = $shell.CreateShortcut($shortcutPath) |
| $shortcut.TargetPath = $DesktopCommand |
| $shortcut.WorkingDirectory = $env:USERPROFILE |
| $shortcut.IconLocation = "$IconPath,0" |
| $shortcut.Description = "Run Free Claude Code in the background" |
| $shortcut.Save() |
| } |
| } |
|
|
| 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." |
| } |
|
|
| Add-KnownBinDirectories |
|
|
| Write-Step "Checking for running Free Claude Code processes" |
| Assert-NoFccProcessesRunning |
|
|
| if (Test-InteractiveInstaller) { |
| Write-Step "Choosing coding agents" |
| Select-CodingAgents |
| } |
|
|
| Ensure-SelectedCodingAgents |
|
|
| Write-Step "Ensuring uv $MinUvVersion or newer is installed" |
| Ensure-Uv |
|
|
| Write-Step "Installing or updating Free Claude Code" |
| Install-FreeClaudeCode |
|
|
| Write-Step "Configuring PATH and verifying Free Claude Code" |
| Configure-AndConfirmFreeClaudeCode |
|
|
| Write-Host "" |
| if ($DryRun) { |
| Write-Host "Dry run complete. No changes were made." |
| } |
| else { |
| Write-Host "Free Claude Code is installed and verified. Open the Free Claude Code desktop shortcut to run it in the background." |
| Write-Host "For terminal use, start the proxy with: fcc-server" |
| if ($script:InstallClaudeCode) { |
| Write-Host "Run Claude Code with: fcc-claude" |
| } |
| if ($script:InstallCodex) { |
| Write-Host "Run Codex with: fcc-codex" |
| } |
| if ($script:PiAvailable) { |
| Write-Host "Run Pi with: fcc-pi" |
| } |
| } |
|
|