Private
Public Access
1
0
Files
xcodecli-shells/setup.ps1
sususu bb74d0f720 fix: npm install -g 后智能搜索全局 bin 路径避免命令找不到
npm install -g 成功后命令可能不在 PATH 中(尤其使用 fnm 管理的 Node.js)。
安装后通过 npm config get prefix 和 %APPDATA%\npm 查找全局 bin 目录
并自动加入当前会话 PATH。
2026-03-20 23:47:40 +08:00

437 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'; 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 <KEY> 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