# Sundy Handbook ยท Claude Code installer for Windows # # Recommended: # try { $env:CLAUDE_SUNDY_NO_PAUSE='1'; $u='https://sundy.tumpai.site/install-claude.ps1'; iex ([System.Text.Encoding]::UTF8.GetString((New-Object Net.WebClient).DownloadData($u))) } catch { Write-Host ('Installer failed to start: ' + $_.Exception.Message) -ForegroundColor Red } finally { if (-not [Console]::IsInputRedirected) { Read-Host 'Press Enter to exit' } } # # Unattended: # $env:SUNDY_API_KEY='sk-xxx'; $env:CLAUDE_SUNDY_YES='1'; try { $env:CLAUDE_SUNDY_NO_PAUSE='1'; $u='https://sundy.tumpai.site/install-claude.ps1'; iex ([System.Text.Encoding]::UTF8.GetString((New-Object Net.WebClient).DownloadData($u))) } catch { Write-Host ('Installer failed to start: ' + $_.Exception.Message) -ForegroundColor Red } finally { if (-not [Console]::IsInputRedirected) { Read-Host 'Press Enter to exit' } } $ErrorActionPreference = "Stop" try { [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false) $OutputEncoding = [Console]::OutputEncoding if ($PSVersionTable.PSEdition -eq "Desktop") { chcp 65001 > $null } [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12 } catch {} $LogFile = Join-Path $env:TEMP "claude-sundy-install-last.log" function Add-Log { param([string]$Message) if (-not $LogFile) { return } try { Add-Content -Encoding UTF8 -Path $LogFile -Value $Message } catch {} } try { "Claude Code installer started at $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" | Set-Content -Encoding UTF8 -Path $LogFile } catch {} $BaseUrl = if ($env:ANTHROPIC_BASE_URL) { $env:ANTHROPIC_BASE_URL } elseif ($env:CLAUDE_BASE_URL) { $env:CLAUDE_BASE_URL } else { "https://sundy-api.tumpai.site:2053" } $BaseUrl = $BaseUrl.TrimEnd("/") if ($BaseUrl.EndsWith("/v1")) { $BaseUrl = $BaseUrl.Substring(0, $BaseUrl.Length - 3).TrimEnd("/") } $NonInteractive = $false try { if ([Console]::IsInputRedirected) { $NonInteractive = $true } } catch { $NonInteractive = $true } $AssumeYes = ($env:CLAUDE_SUNDY_YES -eq "1") -or ($env:CLAUDE_YES -eq "1") -or $NonInteractive function Write-Title { param([string]$Message) Write-Host "" Write-Host "==> $Message" -ForegroundColor Cyan Add-Log "==> $Message" } function Write-Ok { param([string]$Message) Write-Host "OK: $Message" -ForegroundColor Green Add-Log "OK: $Message" } function Write-Info { param([string]$Message) Write-Host "INFO: $Message" -ForegroundColor Cyan Add-Log "INFO: $Message" } function Write-Warn2 { param([string]$Message) Write-Host "WARN: $Message" -ForegroundColor Yellow Add-Log "WARN: $Message" } function Write-Fail { param([string]$Message) throw $Message } function Wait-BeforeExit { if ($env:CLAUDE_SUNDY_NO_PAUSE -eq "1" -or $env:CI -eq "true") { return } try { Write-Host "" Read-Host "Press Enter to exit" | Out-Null } catch { try { cmd.exe /c pause | Out-Null } catch {} } } function Handle-InstallError { param([object]$Err) Write-Host "" Write-Host ("ERROR: Installation stopped: " + $Err.Exception.Message) -ForegroundColor Red Add-Log ("ERROR: " + $Err.Exception.Message) if ($LogFile) { Write-Host ("Log file: " + $LogFile) -ForegroundColor Yellow } Wait-BeforeExit } function Write-Utf8NoBom { param([string]$Path, [string]$Content) $utf8NoBom = [System.Text.UTF8Encoding]::new($false) [System.IO.File]::WriteAllText($Path, $Content, $utf8NoBom) } function Read-Line { param([string]$Prompt, [string]$Default = "") if ($NonInteractive) { return $Default } try { $value = Read-Host $Prompt if ([string]::IsNullOrWhiteSpace($value)) { return $Default } return $value } catch { Write-Warn2 "Read-Host failed. Using default: $Default" return $Default } } function Refresh-ProcessPath { $parts = @( [System.Environment]::GetEnvironmentVariable("Path", "Machine"), [System.Environment]::GetEnvironmentVariable("Path", "User"), (Join-Path $HOME ".local\bin"), (Join-Path $HOME ".claude\local") ) if ($env:LOCALAPPDATA) { $parts += (Join-Path $env:LOCALAPPDATA "Microsoft\WindowsApps") } if ($env:APPDATA) { $parts += (Join-Path $env:APPDATA "npm") } $env:Path = ($parts | Where-Object { $_ -and $_.Trim() } | Select-Object -Unique) -join ";" } function Get-TextFilePreview { param([string]$Path, [int]$MaxChars = 4096) try { $text = [System.IO.File]::ReadAllText($Path, [System.Text.Encoding]::UTF8) if ($text.Length -le $MaxChars) { return $text } return $text.Substring(0, $MaxChars) } catch { return "" } } function Test-PowerShellInstallerFile { param([string]$Path) if (-not (Test-Path $Path)) { return "downloaded file is missing" } $item = Get-Item $Path if ($item.Length -lt 20) { return "downloaded file is empty or too small" } $preview = Get-TextFilePreview -Path $Path if ([string]::IsNullOrWhiteSpace($preview)) { return "downloaded file is not readable as text" } $trimmed = $preview.TrimStart() if ($trimmed.StartsWith([string][char]0xFEFF)) { $trimmed = $trimmed.Substring(1).TrimStart() } $lowerPreview = $preview.ToLowerInvariant() $lowerHead = $trimmed if ($lowerHead.Length -gt 256) { $lowerHead = $lowerHead.Substring(0, 256) } $lowerHead = $lowerHead.ToLowerInvariant() if ($lowerHead.StartsWith("") -or $lowerPreview.Contains("