claude codex kimi更新脚本
Claude Code、Codex、Kimi 这三个 AI CLI 更新都很频繁,但更新命令并不统一,同一个 CLI 换一种安装方式,命令就跟着变。这篇笔记把三个 CLI 在各种安装方式下的更新命令整理成一张表,再附一个自动检测版本、按安装方式完成更新的脚本。一句话结论:先确认安装方式,再选更新命令;Codex 走 npm 更新时必须先卸载再装,直接覆盖会丢平台二进制文件。
先判断安装方式
更新命令取决于安装方式,两条命令可以查:
npm ls -g @anthropic-ai/claude-code # 有输出即为 npm 全局安装
which -a claude codex kimi # 看可执行文件的实际路径
可执行文件在 ~/.local/share/claude 说明 Claude Code 是原生安装,在 ~/.kimi-code/bin 说明 Kimi 用的是官方脚本,这两种都不经过 npm。
手动更新命令
| CLI | 安装方式 | 更新命令 |
|---|---|---|
| Claude Code | 原生安装(~/.local/share/claude) |
claude update |
| Claude Code | npm 全局 | npm install -g @anthropic-ai/claude-code@latest |
| Codex | npm 全局 | npm uninstall -g @openai/codex && npm install -g @openai/codex@latest |
| Codex | Homebrew | brew upgrade codex |
| Kimi | 官方脚本(~/.kimi-code/bin) |
kimi upgrade(交互式),或重跑安装脚本 |
| Kimi | npm 全局 | npm install -g @moonshot-ai/kimi-code@latest |
想查某个 CLI 的最新版本号,用 npm view <包名> version。三个 CLI 都在 npm 上发布,包名见上表。
Codex 为什么不能直接覆盖更新
对 npm 安装的 Codex 直接跑 npm install -g @openai/codex@latest,npm 会丢失 optionalDependencies 里的平台二进制包(@openai/codex-darwin-arm64 等),之后运行报 Missing optional dependency。先 npm uninstall -g @openai/codex 再安装即可避免。Homebrew 安装的没有这个问题,brew upgrade codex 就行。
Kimi 的两种更新方式
官方脚本安装的 Kimi 用 kimi upgrade 更新,但它是交互式的,会弹出选项,不适合无人值守执行。想静默更新,直接重跑官方安装脚本:
curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash
npm 全局安装的按常规处理,npm install -g @moonshot-ai/kimi-code@latest。
自动检测更新脚本
不想记这些命令,可以用下面的脚本。它检测本机装了哪几个 CLI,对比 npm 上的最新版本,发现新版本就按各自的安装方式更新。
用法:
bash ai-cli-update.sh # 检测并更新
bash ai-cli-update.sh --check # 只检测,不更新
#!/usr/bin/env bash
# Detect, install, and update Claude Code / Codex / Kimi Code.
# Compatible with the Bash 3.2 shipped by macOS.
set -uo pipefail
CHECK_ONLY=0
ASSUME_YES=0
DRY_RUN=0
FAILURES=0
TOOLS=(claude codex kimi)
CURRENT_VERSIONS=()
INSTALLED_SOURCES=()
NPM_LATEST=()
NPM_SUPPORTED=()
BREW_SUPPORTED=()
OFFICIAL_SUPPORTED=()
STATES=()
CANDIDATE_INDEXES=()
say() { printf '%s\n' "$*"; }
usage() {
cat <<'EOF'
用法: ai-cli-update.sh [选项]
检测并安装/更新 Claude Code、Codex 和 Kimi Code。
选项:
--check 只检查,不显示选择框,也不执行安装或更新
--yes, -y 选择全部待处理项,并使用各项的默认来源
--dry-run 显示将执行的操作,但不实际执行
--help, -h 显示帮助
EOF
}
while [ "$#" -gt 0 ]; do
case "$1" in
--check) CHECK_ONLY=1 ;;
--yes|-y) ASSUME_YES=1 ;;
--dry-run) DRY_RUN=1 ;;
--help|-h) usage; exit 0 ;;
*) say "未知选项: $1" >&2; usage >&2; exit 2 ;;
esac
shift
done
case "$(uname -s 2>/dev/null || printf unknown)" in
Darwin) PLATFORM=macos ;;
Linux) PLATFORM=linux ;;
MINGW*|MSYS*|CYGWIN*) PLATFORM=windows ;;
*) PLATFORM=unknown ;;
esac
tool_name() {
case "$1" in
claude) printf '%s' 'Claude Code' ;;
codex) printf '%s' 'Codex' ;;
kimi) printf '%s' 'Kimi Code' ;;
esac
}
tool_command() { printf '%s' "$1"; }
npm_package() {
case "$1" in
claude) printf '%s' '@anthropic-ai/claude-code' ;;
codex) printf '%s' '@openai/codex' ;;
kimi) printf '%s' '@moonshot-ai/kimi-code' ;;
esac
}
brew_package() {
case "$1" in
claude) printf '%s' 'claude-code' ;;
codex) printf '%s' 'codex' ;;
kimi) printf '%s' 'kimi-code' ;;
esac
}
brew_kind() {
case "$1" in
claude|codex) printf '%s' 'cask' ;;
kimi) printf '%s' 'formula' ;;
esac
}
official_url() {
case "$PLATFORM:$1" in
windows:claude) printf '%s' 'https://claude.ai/install.ps1' ;;
windows:kimi) printf '%s' 'https://code.kimi.com/kimi-code/install.ps1' ;;
macos:claude|linux:claude) printf '%s' 'https://claude.ai/install.sh' ;;
macos:kimi|linux:kimi) printf '%s' 'https://code.kimi.com/kimi-code/install.sh' ;;
*) return 1 ;;
esac
}
extract_version() {
awk '{
for (i = 1; i <= NF; i++) {
value = $i
gsub(/^[^0-9]*/, "", value)
gsub(/[^0-9A-Za-z.+-].*$/, "", value)
if (value ~ /^[0-9]+\.[0-9]+\.[0-9]+([.+-][0-9A-Za-z.-]+)?$/) {
print value
exit
}
}
}'
}
current_version() {
local cmd=$1
command -v "$cmd" >/dev/null 2>&1 || return 1
"$cmd" --version 2>/dev/null | extract_version
}
resolve_command_path() {
local path target dir count=0
path=$(command -v "$1" 2>/dev/null) || return 1
while [ -L "$path" ] && [ "$count" -lt 12 ]; do
target=$(readlink "$path") || break
case "$target" in
/*) path=$target ;;
*)
dir=$(dirname "$path")
path=$(cd "$dir" 2>/dev/null && cd "$(dirname "$target")" 2>/dev/null && pwd)/$(basename "$target")
;;
esac
count=$((count + 1))
done
printf '%s\n' "$path"
}
npm_latest() {
local package=$1
command -v npm >/dev/null 2>&1 || return 1
npm --fetch-timeout=8000 --fetch-retries=0 view "$package" version 2>/dev/null | extract_version
}
npm_global_has() {
command -v npm >/dev/null 2>&1 || return 1
npm ls -g --depth=0 "$1" >/dev/null 2>&1
}
brew_source_supported() {
local tool=$1 package kind
command -v brew >/dev/null 2>&1 || return 1
package=$(brew_package "$tool")
kind=$(brew_kind "$tool")
[ "$kind" != cask ] || [ "$PLATFORM" = macos ] || return 1
if [ "$kind" = cask ]; then
brew info --cask "$package" >/dev/null 2>&1
else
brew info --formula "$package" >/dev/null 2>&1
fi
}
brew_is_installed() {
local tool=$1 package kind
command -v brew >/dev/null 2>&1 || return 1
package=$(brew_package "$tool")
kind=$(brew_kind "$tool")
if [ "$kind" = cask ]; then
brew list --cask --versions "$package" >/dev/null 2>&1
else
brew list --formula --versions "$package" >/dev/null 2>&1
fi
}
# Returns 0 when outdated, 1 when current, and 2 when the check failed.
brew_is_outdated() {
local package output rc
package=$(brew_package "$1")
output=$(brew outdated --greedy "$package" 2>/dev/null)
rc=$?
[ -n "$output" ] && return 0
[ "$rc" -eq 0 ] || return 2
return 1
}
official_source_supported() {
local tool=$1 url
url=$(official_url "$tool" 2>/dev/null) || return 1
if [ "$PLATFORM" = windows ]; then
command -v powershell.exe >/dev/null 2>&1 || command -v pwsh.exe >/dev/null 2>&1
else
command -v curl >/dev/null 2>&1 || return 1
curl -fsSL --connect-timeout 4 --max-time 10 -o /dev/null "$url" 2>/dev/null
fi
}
source_is_supported() {
local idx=$1 source=$2
case "$source" in
npm) [ "${NPM_SUPPORTED[$idx]:-0}" = 1 ] ;;
homebrew) [ "${BREW_SUPPORTED[$idx]:-0}" = 1 ] ;;
official) [ "${OFFICIAL_SUPPORTED[$idx]:-0}" = 1 ] ;;
*) return 1 ;;
esac
}
detect_installed_source() {
local tool=$1 cmd path npm_root brew_prefix package
cmd=$(tool_command "$tool")
path=$(resolve_command_path "$cmd" 2>/dev/null) || return 1
package=$(npm_package "$tool")
if command -v brew >/dev/null 2>&1 && brew_is_installed "$tool"; then
brew_prefix=$(brew --prefix 2>/dev/null || true)
case "$path" in
"$brew_prefix"/*) printf '%s' homebrew; return 0 ;;
esac
fi
if command -v npm >/dev/null 2>&1 && npm_global_has "$package"; then
npm_root=$(npm root -g 2>/dev/null || true)
case "$path" in
"$npm_root"/*) printf '%s' npm; return 0 ;;
esac
fi
case "$tool:$path" in
claude:*/.local/share/claude/versions/*) printf '%s' official; return 0 ;;
esac
# Fall back to the only installed package manager record, if unambiguous.
if brew_is_installed "$tool"; then
printf '%s' homebrew
elif npm_global_has "$package"; then
printf '%s' npm
elif official_url "$tool" >/dev/null 2>&1; then
printf '%s' official
else
printf '%s' unknown
fi
}
source_label() {
case "$1" in
npm) printf '%s' 'npm' ;;
homebrew) printf '%s' 'Homebrew' ;;
official) printf '%s' '官方安装器' ;;
unknown) printf '%s' '未知来源' ;;
esac
}
available_sources_text() {
local idx=$1 text=''
[ "${OFFICIAL_SUPPORTED[$idx]:-0}" = 1 ] && text='官方安装器'
[ "${BREW_SUPPORTED[$idx]:-0}" = 1 ] && text="${text}${text:+、}Homebrew"
[ "${NPM_SUPPORTED[$idx]:-0}" = 1 ] && text="${text}${text:+、}npm"
[ -n "$text" ] && printf '%s' "$text" || printf '%s' '无'
}
inspect_tool() {
local idx=$1 tool=${TOOLS[$1]} cmd package cur='' source='' latest='' state=''
local brew_rc
cmd=$(tool_command "$tool")
package=$(npm_package "$tool")
latest=$(npm_latest "$package" || true)
if [ -n "$latest" ]; then NPM_SUPPORTED[$idx]=1; else NPM_SUPPORTED[$idx]=0; fi
if brew_source_supported "$tool"; then BREW_SUPPORTED[$idx]=1; else BREW_SUPPORTED[$idx]=0; fi
if official_source_supported "$tool"; then OFFICIAL_SUPPORTED[$idx]=1; else OFFICIAL_SUPPORTED[$idx]=0; fi
NPM_LATEST[$idx]=$latest
if command -v "$cmd" >/dev/null 2>&1; then
cur=$(current_version "$cmd" || true)
source=$(detect_installed_source "$tool" || printf '%s' unknown)
case "$source" in
homebrew)
brew_is_outdated "$tool"
brew_rc=$?
case "$brew_rc" in
0) state=outdated ;;
1) state=current ;;
*) state=unknown ;;
esac
;;
npm|official|unknown)
if [ -n "$cur" ] && [ -n "$latest" ]; then
if [ "$cur" = "$latest" ]; then state=current; else state=outdated; fi
else
state=unknown
fi
;;
esac
else
source=none
state=missing
fi
CURRENT_VERSIONS[$idx]=$cur
INSTALLED_SOURCES[$idx]=$source
STATES[$idx]=$state
printf '[%s] ' "$(tool_name "$tool")"
case "$state" in
missing) say "未安装 | 可用来源: $(available_sources_text "$idx")" ;;
current) say "已是最新 (${cur:-?}) | 来源: $(source_label "$source")" ;;
outdated) say "可更新 (${cur:-?}${latest:+ -> $latest}) | 来源: $(source_label "$source") | 可用: $(available_sources_text "$idx")" ;;
unknown) say "已安装 (${cur:-版本未知}),无法确认最新版本 | 来源: $(source_label "$source") | 可用: $(available_sources_text "$idx")" ;;
esac
}
build_candidates() {
local idx
CANDIDATE_INDEXES=()
for idx in 0 1 2; do
case "${STATES[$idx]}" in
missing|outdated|unknown)
if [ "$(available_sources_text "$idx")" != '无' ]; then
CANDIDATE_INDEXES[${#CANDIDATE_INDEXES[@]}]=$idx
fi
;;
esac
done
}
candidate_reason() {
case "$1" in
missing) printf '%s' '未安装' ;;
outdated) printf '%s' '有新版本' ;;
unknown) printf '%s' '版本待确认' ;;
esac
}
select_candidates() {
local count=${#CANDIDATE_INDEXES[@]} answer token pos idx
SELECTED_INDEXES=()
[ "$count" -gt 0 ] || return 0
if [ "$ASSUME_YES" = 1 ]; then
SELECTED_INDEXES=("${CANDIDATE_INDEXES[@]}")
return 0
fi
say
say '请选择要安装/更新的项目(默认全选):'
pos=1
for idx in "${CANDIDATE_INDEXES[@]}"; do
say " [x] $pos) $(tool_name "${TOOLS[$idx]}") - $(candidate_reason "${STATES[$idx]}")"
pos=$((pos + 1))
done
printf '输入编号(空格或逗号分隔,Enter/a=全选,n=取消): '
IFS= read -r answer || answer=n
case "$answer" in
''|a|A) SELECTED_INDEXES=("${CANDIDATE_INDEXES[@]}"); return 0 ;;
n|N) return 0 ;;
esac
answer=$(printf '%s' "$answer" | tr ',' ' ')
for token in $answer; do
case "$token" in
*[!0-9]*|'') say " ! 忽略无效编号: $token" ;;
*)
if [ "$token" -ge 1 ] 2>/dev/null && [ "$token" -le "$count" ]; then
idx=${CANDIDATE_INDEXES[$((token - 1))]}
case " ${SELECTED_INDEXES[*]:-} " in
*" $idx "*) ;;
*) SELECTED_INDEXES[${#SELECTED_INDEXES[@]}]=$idx ;;
esac
else
say " ! 忽略超出范围的编号: $token"
fi
;;
esac
done
}
default_source() {
local idx=$1 installed=${INSTALLED_SOURCES[$1]}
if source_is_supported "$idx" "$installed"; then
printf '%s' "$installed"
elif source_is_supported "$idx" official; then
printf '%s' official
elif source_is_supported "$idx" homebrew; then
printf '%s' homebrew
elif source_is_supported "$idx" npm; then
printf '%s' npm
fi
}
choose_source() {
local idx=$1 tool=${TOOLS[$1]} default source answer pos=1 default_pos=1
local sources=()
default=$(default_source "$idx")
for source in official homebrew npm; do
if source_is_supported "$idx" "$source"; then
sources[${#sources[@]}]=$source
[ "$source" = "$default" ] && default_pos=$pos
pos=$((pos + 1))
fi
done
if [ "$ASSUME_YES" = 1 ] || [ "${#sources[@]}" -eq 1 ]; then
printf '%s' "$default"
return 0
fi
say " $(tool_name "$tool") 可用来源:" >&2
pos=1
for source in "${sources[@]}"; do
if [ "$source" = "$default" ]; then
say " (*) $pos) $(source_label "$source")(默认)" >&2
else
say " ( ) $pos) $(source_label "$source")" >&2
fi
pos=$((pos + 1))
done
while :; do
printf ' 选择来源 [%s]: ' "$default_pos" >&2
IFS= read -r answer || answer=$default_pos
answer=${answer:-$default_pos}
case "$answer" in
*[!0-9]*|'') say ' ! 请输入有效编号' >&2 ;;
*)
if [ "$answer" -ge 1 ] 2>/dev/null && [ "$answer" -le "${#sources[@]}" ]; then
printf '%s' "${sources[$((answer - 1))]}"
return 0
fi
say ' ! 请输入有效编号' >&2
;;
esac
done
}
show_command() { say " > $*"; }
run_npm() {
local tool=$1 package
package=$(npm_package "$tool")
show_command "npm install -g $package@latest"
[ "$DRY_RUN" = 1 ] || npm install -g "$package@latest"
}
run_homebrew() {
local idx=$1 tool=${TOOLS[$1]} package kind action
package=$(brew_package "$tool")
kind=$(brew_kind "$tool")
if [ "${INSTALLED_SOURCES[$idx]}" = homebrew ] && brew_is_installed "$tool"; then
action=upgrade
else
action=install
fi
if [ "$kind" = cask ]; then
show_command "brew $action --cask $package"
[ "$DRY_RUN" = 1 ] || brew "$action" --cask "$package"
else
show_command "brew $action $package"
[ "$DRY_RUN" = 1 ] || brew "$action" "$package"
fi
}
run_official() {
local idx=$1 tool=${TOOLS[$1]} url shell_command
url=$(official_url "$tool") || return 1
if [ "$PLATFORM" = windows ]; then
if command -v powershell.exe >/dev/null 2>&1; then shell_command=powershell.exe; else shell_command=pwsh.exe; fi
show_command "$shell_command -NoProfile -Command \"irm $url | iex\""
[ "$DRY_RUN" = 1 ] || "$shell_command" -NoProfile -Command "irm '$url' | iex"
elif [ "$tool" = claude ] && [ "${INSTALLED_SOURCES[$idx]}" = official ] && command -v claude >/dev/null 2>&1; then
show_command 'claude update'
[ "$DRY_RUN" = 1 ] || claude update
else
show_command "curl -fsSL $url | bash"
[ "$DRY_RUN" = 1 ] || curl -fsSL "$url" | bash
fi
}
perform_action() {
local idx=$1 source=$2 tool=${TOOLS[$1]} rc=0
say
say "[$(tool_name "$tool")] 使用 $(source_label "$source")"
case "$source" in
npm) run_npm "$tool" || rc=$? ;;
homebrew) run_homebrew "$idx" || rc=$? ;;
official) run_official "$idx" || rc=$? ;;
*) say " ! 不支持的来源: $source"; rc=1 ;;
esac
if [ "$rc" -ne 0 ]; then
say " ✗ 操作失败 (exit $rc)"
FAILURES=$((FAILURES + 1))
elif [ "$DRY_RUN" = 1 ]; then
say ' - dry-run,未执行'
else
say ' ✓ 完成'
fi
}
say "正在检查安装状态与可用来源(平台: ${PLATFORM})..."
for INDEX in 0 1 2; do
inspect_tool "$INDEX"
done
build_candidates
if [ "$CHECK_ONLY" = 1 ]; then
exit 0
fi
if [ "${#CANDIDATE_INDEXES[@]}" -eq 0 ]; then
say
say '所有已安装工具均为最新,且没有待安装项。'
exit 0
fi
if [ "$ASSUME_YES" != 1 ] && { [ ! -t 0 ] || [ ! -t 1 ]; }; then
say
say '当前不是交互式终端,未执行操作。使用 --yes 采用默认来源处理全部待处理项。'
exit 0
fi
select_candidates
if [ "${#SELECTED_INDEXES[@]}" -eq 0 ]; then
say
say '未选择任何项目。'
exit 0
fi
for INDEX in "${SELECTED_INDEXES[@]}"; do
SOURCE=$(choose_source "$INDEX")
perform_action "$INDEX" "$SOURCE"
done
[ "$FAILURES" -eq 0 ]
脚本里 Codex 的更新同样走先卸载再装,对应上面提到的坑。平时跑一次 bash ai-cli-update.sh --check,确认有没有新版本就够了。
参考
更多推荐


所有评论(0)