Private
Public Access
1
0
Files
xcodecli-shells/setup.ps1
sususu98 3e5676872d fix: 移除 BOM 并增强 UTF-8 编码修复
- 移除 UTF-8 BOM (BOM 会破坏 iwr|iex 管道导致 param() 报错)
- 去掉 $Host.Name 守卫,无条件设置 Console 编码 (try/catch 兜底)
- chcp 65001 先于 Console.OutputEncoding 设置,确保代码页一致
2026-02-28 15:56:21 +08:00

419 lines
13 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# XCodeCLI Setup Launcher (Windows)
# 一站式安装和配置 Claude Code, Gemini CLI, Codex
param(
[string]$ApiKey,
[switch]$Help
)
# 支持一行命令传入 API Key: $key='xxx'; iwr ... | iex
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 <KEY> API
-Help
:
- Node.js >= 20.x
- Node.js fnm Node.js 24.x
使:
`$key='YOUR_API_KEY'; iwr -useb https://gitea.sususu.cf/sususu/xcodecli-shells/raw/branch/main/setup.ps1 | iex
"@
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 安装 ==========
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 --accept-source-agreements --accept-package-agreements
}
else {
# 备选:使用 cargo 或手动下载
Write-Info "使用 PowerShell 脚本安装 fnm..."
Invoke-Expression "& { $(Invoke-RestMethod https://fnm.vercel.app/install.ps1) }"
}
Refresh-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
}
return $true
}
else {
Write-Warning "fnm 可能已安装,但需要重新打开终端才能生效"
Write-Info "请重新打开 PowerShell 后再运行此脚本"
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
& fnm use 24
& fnm default 24
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
)
if ($Tool.Command -eq "claude") {
# Claude Code: 使用官方原生安装脚本
Write-Info "使用官方安装脚本安装 $($Tool.Name)..."
Write-Host " 执行: irm https://claude.ai/install.ps1 | iex" -ForegroundColor Gray
try {
Invoke-Expression "& { $(Invoke-RestMethod https://claude.ai/install.ps1) }"
Refresh-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
}
}
else {
# Gemini CLI / Codex: 使用 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
$exitCode = $LASTEXITCODE
Refresh-Path
if ($exitCode -ne 0) {
Write-Error "安装命令返回错误码: $exitCode"
return $false
}
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
}
else {
& $tempFile
}
$scriptSucceeded = $?
$exitCode = $LASTEXITCODE
# 清理临时文件
Remove-Item $tempFile -ErrorAction SilentlyContinue
# 检查执行结果
if (-not $scriptSucceeded -or $exitCode -ne 0) {
Write-Error "配置脚本执行失败,退出码: $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