# XCodeCLI Setup Launcher (Windows) # 一站式安装和配置 Claude Code, Gemini CLI, Codex param( [string]$ApiKey, [switch]$Help ) # 支持一行命令传入 API Key: $key='xxx'; iex (irm URL) if (-not $ApiKey -and (Test-Path Variable:key)) { $ApiKey = $key } # ========== UTF-8 编码兼容 ========== # 确保中文字符在各种 PowerShell 环境中正确显示 # 兼容: Windows PowerShell 5.x, PowerShell 7.x, Windows Terminal, conhost, ISE, iwr|iex try { # 1. 先设置控制台代码页为 UTF-8 if ($env:OS -eq 'Windows_NT') { & cmd /c "chcp 65001 >nul" 2>&1 | Out-Null } # 2. 设置 .NET 控制台编码 (Write-Host 依赖此设置) [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::InputEncoding = [System.Text.Encoding]::UTF8 # 3. 设置 PowerShell 管道/cmdlet 输出编码 $OutputEncoding = [System.Text.Encoding]::UTF8 } catch { # 静默处理 - ISE 等环境可能不支持 Console 类操作 } # ========== 颜色输出函数 ========== function Write-Info { param([string]$Message); Write-Host "[INFO]" -ForegroundColor Blue -NoNewline; Write-Host " $Message" } function Write-Success { param([string]$Message); Write-Host "[SUCCESS]" -ForegroundColor Green -NoNewline; Write-Host " $Message" } function Write-Warning { param([string]$Message); Write-Host "[WARNING]" -ForegroundColor Yellow -NoNewline; Write-Host " $Message" } function Write-Error { param([string]$Message); Write-Host "[ERROR]" -ForegroundColor Red -NoNewline; Write-Host " $Message" } # ========== 工具配置表 ========== $ToolsConfig = @{ 1 = @{ Name = "Claude Code" Command = "claude" Package = "@anthropic-ai/claude-code" SetupUrl = "https://gitea.sususu.cf/sususu/xcodecli-shells/raw/branch/main/ClaudeCode/setup-claude-code.ps1" } 2 = @{ Name = "Gemini CLI" Command = "gemini" Package = "@google/gemini-cli@latest" SetupUrl = "https://gitea.sususu.cf/sususu/xcodecli-shells/raw/branch/main/GeminiCLI/setup-gemini.ps1" } 3 = @{ Name = "Codex" Command = "codex" Package = "@openai/codex" SetupUrl = "https://gitea.sususu.cf/sususu/xcodecli-shells/raw/branch/main/codex/setup-codex.ps1" } } # ========== 帮助信息 ========== function Show-Help { Write-Host @" XCodeCLI Setup Launcher (Windows) 一站式安装和配置 Claude Code, Gemini CLI, Codex Usage: powershell -ExecutionPolicy Bypass -File setup.ps1 [OPTIONS] Options: -ApiKey 预设 API 密钥(可选) -Help 显示此帮助信息 环境要求: - Node.js >= 20.x - 若未安装 Node.js,脚本会自动通过 fnm 安装 Node.js 24.x 一行命令快速使用: `$key='YOUR_API_KEY'; $f="$env:TEMP\xc.ps1";iwr -useb https://gitea.sususu.cf/sususu/xcodecli-shells/raw/branch/main/setup.ps1 -OutFile $f;& $f "@ exit 0 } # ========== 工具函数 ========== function Test-Command { param([string]$Name) return [bool](Get-Command $Name -ErrorAction SilentlyContinue) } function Refresh-Path { $env:Path = [Environment]::GetEnvironmentVariable("Path", "User") + ";" + [Environment]::GetEnvironmentVariable("Path", "Machine") } # ========== Node.js 版本检测 ========== function Get-NodeVersion { if (Test-Command "node") { try { $versionStr = (& node --version 2>$null) -replace 'v', '' $major = [int]($versionStr -split '\.')[0] return @{ Version = $versionStr; Major = $major } } catch { } } return $null } # ========== fnm PATH 搜索 ========== function Find-FnmPath { # 在常见安装位置搜索 fnm.exe $searchPaths = @( "$env:LOCALAPPDATA\Microsoft\WinGet\Links", "$env:LOCALAPPDATA\Microsoft\WinGet\Packages\Schniz.fnm_*", "$env:USERPROFILE\.fnm", "$env:LOCALAPPDATA\fnm", "$env:ProgramFiles\fnm" ) foreach ($pattern in $searchPaths) { $resolved = Resolve-Path $pattern -ErrorAction SilentlyContinue foreach ($dir in $resolved) { $fnmExe = Get-ChildItem -Path $dir.Path -Filter "fnm.exe" -Recurse -Depth 2 -ErrorAction SilentlyContinue | Select-Object -First 1 if ($fnmExe) { return $fnmExe.DirectoryName } } } return $null } # ========== fnm 安装 ========== function Install-Fnm { Write-Host "" Write-Info "正在安装 fnm (Fast Node Manager)..." try { # 使用 winget 安装 fnm if (Test-Command "winget") { Write-Info "使用 winget 安装 fnm..." & winget install Schniz.fnm -e --source winget --accept-source-agreements --accept-package-agreements | Out-Host } else { # 备选:使用 PowerShell 脚本安装 fnm Write-Info "使用 PowerShell 脚本安装 fnm..." Invoke-Expression "& { $(Invoke-RestMethod https://fnm.vercel.app/install.ps1) }" | Out-Host } Refresh-Path # 如果 fnm 不在 PATH 中,搜索常见安装位置 if (-not (Test-Command "fnm")) { Write-Info "正在搜索 fnm 安装位置..." $fnmDir = Find-FnmPath if ($fnmDir) { Write-Info "在 $fnmDir 找到 fnm" $env:Path = "$fnmDir;$env:Path" } } # 配置 fnm 环境 if (Test-Command "fnm") { Write-Success "fnm 安装成功!" # 初始化 fnm $fnmEnv = & fnm env --use-on-cd 2>$null if ($fnmEnv) { $fnmEnv | Out-String | Invoke-Expression | Out-Null } return $true } else { Write-Warning "fnm 安装后未能在 PATH 中找到,请重新打开终端后再运行此脚本" return $false } } catch { Write-Error "fnm 安装失败: $($_.Exception.Message)" return $false } } # ========== 使用 fnm 安装 Node.js ========== function Install-NodeWithFnm { Write-Info "使用 fnm 安装 Node.js 24.x..." try { & fnm install 24 | Out-Host & fnm use 24 | Out-Host & fnm default 24 | Out-Host Refresh-Path if (Test-Command "node") { $nodeInfo = Get-NodeVersion Write-Success "Node.js v$($nodeInfo.Version) 安装成功!" return $true } else { Write-Warning "Node.js 可能已安装,但需要重新打开终端才能生效" return $false } } catch { Write-Error "Node.js 安装失败: $($_.Exception.Message)" return $false } } # ========== 确保 Node.js 环境就绪 ========== function Ensure-NodeEnvironment { $nodeInfo = Get-NodeVersion if ($nodeInfo) { Write-Info "检测到 Node.js v$($nodeInfo.Version)" if ($nodeInfo.Major -lt 20) { Write-Warning "Node.js 版本过低 (需要 >= 20.x)" Write-Info "请升级 Node.js 或让脚本使用 fnm 安装新版本" $upgrade = Read-Host "是否使用 fnm 安装 Node.js 24.x? (Y/n)" if ($upgrade -eq "n" -or $upgrade -eq "N") { Write-Error "Node.js 版本不满足要求,请手动升级后重试" return $false } # 安装 fnm (如果未安装) if (-not (Test-Command "fnm")) { if (-not (Install-Fnm)) { return $false } } return Install-NodeWithFnm } return $true } # 未安装 Node.js Write-Warning "未检测到 Node.js" Write-Info "将使用 fnm 安装 Node.js 24.x" $install = Read-Host "是否继续? (Y/n)" if ($install -eq "n" -or $install -eq "N") { return $false } # 安装 fnm if (-not (Test-Command "fnm")) { if (-not (Install-Fnm)) { return $false } } return Install-NodeWithFnm } # ========== 工具安装 ========== function Install-Tool { param( [hashtable]$Tool ) # 所有工具统一使用 npm 安装 (需要 Node.js) if (-not (Ensure-NodeEnvironment)) { return $false } Write-Info "使用 npm 安装 $($Tool.Name)..." $installCmd = "npm install -g $($Tool.Package)" Write-Host " 执行: $installCmd" -ForegroundColor Gray try { Invoke-Expression $installCmd | Out-Host $exitCode = $LASTEXITCODE Refresh-Path if ($exitCode -ne 0) { Write-Error "安装命令返回错误码: $exitCode" return $false } # 如果命令不在 PATH 中,尝试查找 npm 全局 bin 目录 if (-not (Test-Command $Tool.Command)) { Write-Info "正在搜索 npm 全局安装位置..." $npmPrefix = & npm config get prefix 2>$null if ($npmPrefix -and (Test-Path $npmPrefix)) { $env:Path = "$npmPrefix;$env:Path" } # 也检查常见的 npm 全局路径 $appDataNpm = "$env:APPDATA\npm" if ((Test-Path $appDataNpm) -and ($env:Path -notlike "*$appDataNpm*")) { $env:Path = "$appDataNpm;$env:Path" } } if (Test-Command $Tool.Command) { Write-Success "$($Tool.Name) 安装成功!" return $true } else { Write-Warning "$($Tool.Name) 可能已安装,但需要重新打开终端才能生效" $continue = Read-Host "是否继续进行配置? (Y/n)" return ($continue -ne "n" -and $continue -ne "N") } } catch { Write-Error "安装失败: $($_.Exception.Message)" return $false } } # ========== 远程配置脚本调用 ========== function Invoke-RemoteSetup { param( [hashtable]$Tool, [string]$ApiKey ) Write-Host "" Write-Info "下载并执行 $($Tool.Name) 配置脚本..." Write-Host " URL: $($Tool.SetupUrl)" -ForegroundColor Gray Write-Host "" $tempFile = $null try { # 下载脚本到临时文件 $tempFile = [System.IO.Path]::ChangeExtension([System.IO.Path]::GetTempFileName(), ".ps1") Invoke-WebRequest -Uri $Tool.SetupUrl -UseBasicParsing -OutFile $tempFile # 执行脚本,正确传递参数 $LASTEXITCODE = 0 if ($ApiKey) { & $tempFile -ApiKey $ApiKey | Out-Host } else { & $tempFile | Out-Host } $scriptSucceeded = $? $exitCode = $LASTEXITCODE # 清理临时文件 Remove-Item $tempFile -ErrorAction SilentlyContinue # 检查执行结果 # 注意: | Out-Host 使 $? 始终为 $true,仅依赖 $LASTEXITCODE if ($exitCode -ne 0) { Write-Warning "配置脚本未完成 (退出码: $exitCode)" return $false } Write-Host "" Write-Success "$($Tool.Name) 配置完成!" return $true } catch { if ($tempFile) { Remove-Item $tempFile -ErrorAction SilentlyContinue } Write-Error "配置脚本执行失败: $($_.Exception.Message)" return $false } } # ========== 菜单显示 ========== function Show-Menu { Write-Host "" Write-Host "========================================" -ForegroundColor Cyan Write-Host " XCodeCLI Setup Launcher" -ForegroundColor Cyan Write-Host "========================================" -ForegroundColor Cyan Write-Host "" # 显示 Node.js 环境状态 $nodeInfo = Get-NodeVersion if ($nodeInfo) { $nodeStatus = if ($nodeInfo.Major -ge 20) { "[OK]" } else { "[需升级]" } $nodeColor = if ($nodeInfo.Major -ge 20) { "Green" } else { "Yellow" } Write-Host " Node.js: v$($nodeInfo.Version) " -NoNewline Write-Host $nodeStatus -ForegroundColor $nodeColor } else { Write-Host " Node.js: " -NoNewline Write-Host "[未安装]" -ForegroundColor Red } Write-Host "" Write-Host "选择要安装和配置的工具:" -ForegroundColor Yellow Write-Host "" foreach ($key in ($ToolsConfig.Keys | Sort-Object)) { $tool = $ToolsConfig[$key] $installed = Test-Command $tool.Command $status = if ($installed) { "[已安装]" } else { "[未安装]" } $color = if ($installed) { "Green" } else { "Red" } $label = " [$key] $($tool.Name.PadRight(12))" Write-Host "$label " -NoNewline Write-Host $status -ForegroundColor $color } Write-Host "" Write-Host " [0] 退出" -ForegroundColor Gray Write-Host "" } # ========== 主函数 ========== function Main { if ($Help) { Show-Help } Show-Menu $choice = Read-Host "请选择 (0-3)" # 退出选项 if ($choice -eq "0") { Write-Info "再见!" exit 0 } # 验证选择 $choiceNum = 0 if (-not [int]::TryParse($choice, [ref]$choiceNum) -or $choiceNum -lt 1 -or $choiceNum -gt 3) { Write-Error "无效的选择" exit 1 } $tool = $ToolsConfig[$choiceNum] # 检测是否已安装 if (-not (Test-Command $tool.Command)) { Write-Host "" Write-Warning "$($tool.Name) 未安装" $install = Read-Host "是否立即安装? (Y/n)" if ($install -eq "n" -or $install -eq "N") { Write-Info "已取消" exit 0 } if (-not (Install-Tool -Tool $tool)) { exit 1 } } else { Write-Host "" Write-Success "$($tool.Name) 已安装" } # 调用远程配置脚本 if (-not (Invoke-RemoteSetup -Tool $tool -ApiKey $ApiKey)) { exit 1 } } Main