Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@ jobs:
fi
echo "✓ Build output matches committed file"

- name: Run proxy regression checks
run: bash scripts/test-socks5h-probes.sh
- name: Run regression checks
run: |
bash scripts/test-socks5h-probes.sh
bash scripts/test-claude-bulk-version-commands.sh

js-check:
name: JS syntax
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ cac claude install latest # 安装最新版
cac claude install 2.1.81 # 安装指定版本
cac claude ls # 列出已安装版本
cac claude pin 2.1.81 # 当前环境切换版本
cac claude update-all latest # 所有环境切换到最新版
cac claude prune # 卸载未被环境使用的版本
cac claude uninstall 2.1.81 # 卸载
```

Expand Down Expand Up @@ -121,6 +123,8 @@ cac ls # = cac env ls
| `cac claude uninstall <ver>` | 卸载版本 |
| `cac claude ls` | 列出已安装版本 |
| `cac claude pin <ver>` | 当前环境绑定版本 |
| `cac claude update-all [latest\|<ver>]` | 所有环境绑定到同一版本 |
| `cac claude prune` | 卸载未被任何环境使用的版本 |
| **环境管理** | |
| `cac env create <name> [-p proxy] [-c ver] [--clone] [--telemetry mode] [--persona preset]` | 创建环境(自动激活,`--telemetry transparent/stealth/paranoid` 控制遥测,`--persona macos-vscode/...` 用于容器) |
| `cac env ls` | 列出环境 |
Expand Down Expand Up @@ -275,6 +279,8 @@ cac claude install latest # install latest
cac claude install 2.1.81 # install specific version
cac claude ls # list installed versions
cac claude pin 2.1.81 # pin current env to version
cac claude update-all latest # pin all envs to latest
cac claude prune # remove versions unused by any env
cac claude uninstall 2.1.81 # remove
```

Expand Down Expand Up @@ -306,6 +312,8 @@ Each environment is fully isolated:
| `cac claude uninstall <ver>` | Remove version |
| `cac claude ls` | List installed versions |
| `cac claude pin <ver>` | Pin current env to version |
| `cac claude update-all [latest\|<ver>]` | Pin all environments to one version |
| `cac claude prune` | Remove versions unused by any environment |
| **Environment management** | |
| `cac env create <name> [-p proxy] [-c ver] [--clone] [--telemetry mode] [--persona preset]` | Create environment (auto-activates, `--telemetry transparent/stealth/paranoid` for telemetry control, `--persona macos-vscode/...` for containers) |
| `cac env ls` | List environments |
Expand Down
60 changes: 60 additions & 0 deletions cac
Original file line number Diff line number Diff line change
Expand Up @@ -2915,6 +2915,60 @@ _claude_cmd_uninstall() {
echo "$(_green_bold "Uninstalled") Claude Code $(_cyan "$ver")"
}

_claude_cmd_update_all() {
local target="${1:-latest}"
local ver
if [[ "$target" == "latest" ]]; then
printf "Fetching latest version ... "
ver=$(_fetch_latest_version) || _die "failed to fetch latest version"
echo "$(_cyan "$ver")"
else
ver="$target"
fi

[[ "$ver" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9._-]+)?$ ]] || \
_die "invalid version $(_cyan "'$ver'")"

mkdir -p "$VERSIONS_DIR"
_download_version "$ver"
_update_latest

local count=0 env_dir
for env_dir in "$ENVS_DIR"/*/; do
[[ -d "$env_dir" ]] || continue
echo "$ver" > "$env_dir/version"
(( count += 1 ))
done

echo "$(_green_bold "Pinned") $count environment(s) -> Claude Code $(_cyan "$ver")"
}

_claude_cmd_prune() {
_update_latest 2>/dev/null || true
if [[ ! -d "$VERSIONS_DIR" ]]; then
echo "$(_dim " No versions installed.")"
return
fi

local removed=0 ver_dir ver count
for ver_dir in "$VERSIONS_DIR"/*/; do
[[ -d "$ver_dir" ]] || continue
ver=$(basename "$ver_dir")
count=$(_envs_using_version "$ver")
if [[ "$count" -eq 0 ]]; then
rm -rf "${VERSIONS_DIR:?}/$ver"
(( removed += 1 ))
fi
done

_update_latest 2>/dev/null || true
if [[ "$removed" -eq 0 ]]; then
echo "$(_dim " No unused versions to prune.")"
else
echo "$(_green_bold "Uninstalled") $removed unused version(s)"
fi
}

_claude_cmd_ls() {
_update_latest 2>/dev/null || true
if [[ ! -d "$VERSIONS_DIR" ]] || [[ -z "$(ls -A "$VERSIONS_DIR" 2>/dev/null)" ]]; then
Expand Down Expand Up @@ -2957,13 +3011,17 @@ cmd_claude() {
case "${1:-help}" in
install) _claude_cmd_install "${@:2}" ;;
uninstall) _claude_cmd_uninstall "${@:2}" ;;
update-all) _claude_cmd_update_all "${@:2}" ;;
prune) _claude_cmd_prune ;;
ls|list) _claude_cmd_ls ;;
pin) _claude_cmd_pin "${@:2}" ;;
help|-h|--help)
echo "$(_bold "cac claude") — Claude Code version management"
echo
echo " $(_bold "install") [latest|<ver>] Install a Claude Code version"
echo " $(_bold "uninstall") <ver> Remove an installed version"
echo " $(_bold "update-all") [latest|<ver>] Pin all environments to a version"
echo " $(_bold "prune") Remove installed versions not used by any environment"
echo " $(_bold "ls") List installed versions"
echo " $(_bold "pin") <ver> Pin current environment to a version"
;;
Expand Down Expand Up @@ -3629,6 +3687,8 @@ cmd_help() {
echo " $(_green "cac claude install") [latest|ver] Install Claude Code"
echo " $(_green "cac claude ls") List installed versions"
echo " $(_green "cac claude pin") <ver> Pin env to a version"
echo " $(_green "cac claude update-all") [latest|ver] Pin all envs to a version"
echo " $(_green "cac claude prune") Remove unused versions"
echo " $(_green "cac claude uninstall") <ver> Remove a version"
echo

Expand Down
8 changes: 8 additions & 0 deletions docs/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ description: Release history and notable changes

All notable changes to this project are documented here. Each entry links to the corresponding PR or issue.

## Unreleased

**Feature: bulk Claude Code version management**

- Added `cac claude update-all [latest|<ver>]` to install a target version if needed and pin every environment to it.
- Added `cac claude prune` to remove installed Claude Code versions that are not used by any environment.
- Added a shell regression test covering bulk pinning and unused-version pruning.

## v1.5.7
<sub>2026-04-27</sub>

Expand Down
21 changes: 21 additions & 0 deletions docs/commands/claude.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,27 @@ cac claude pin 2.1.81

This writes the version to `~/.cac/envs/<name>/version`. The wrapper resolves this file at launch time, so the change takes effect on the next `claude` invocation.

## update-all

Install a target version if needed, then pin every environment to it.

```bash
cac claude update-all latest
cac claude update-all 2.1.81
```

Use this after installing a new Claude Code release when you want all isolated environments to launch the same binary.

## prune

Remove installed versions that are not pinned by any environment.

```bash
cac claude prune
```

This is the bulk cleanup equivalent of running `cac claude uninstall <ver>` for every unused version.

## uninstall

Remove an installed version. Fails if any environment is using it.
Expand Down
8 changes: 8 additions & 0 deletions docs/zh/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ description: 版本发布历史和重要变更

所有重要变更记录在此,每个条目关联对应的 PR 或 Issue。

## Unreleased

**功能:批量管理 Claude Code 版本**

- 新增 `cac claude update-all [latest|<ver>]`,必要时先安装目标版本,然后将所有环境锁定到该版本。
- 新增 `cac claude prune`,移除未被任何环境使用的已安装 Claude Code 版本。
- 新增 shell 回归测试,覆盖批量锁定和未使用版本清理。

## v1.5.7
<sub>2026-04-27</sub>

Expand Down
21 changes: 21 additions & 0 deletions docs/zh/commands/claude.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,27 @@ cac claude pin 2.1.81

这会将版本写入 `~/.cac/envs/<name>/version`。包装器在启动时解析此文件,因此更改会在下次 `claude` 调用时生效。

## update-all

必要时先安装目标版本,然后将所有环境锁定到该版本。

```bash
cac claude update-all latest
cac claude update-all 2.1.81
```

适用于安装新版 Claude Code 后,希望所有隔离环境都启动同一个二进制版本的场景。

## prune

移除未被任何环境锁定的已安装版本。

```bash
cac claude prune
```

这相当于对每个未使用版本批量运行 `cac claude uninstall <ver>`。

## uninstall

移除已安装的版本。如果有环境正在使用该版本,操作会失败。
Expand Down
77 changes: 77 additions & 0 deletions scripts/test-claude-bulk-version-commands.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT="$(cd "$(dirname "$0")/.." && pwd)"
CAC="$ROOT/cac"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT

export HOME="$TMP/home"
CAC_DIR="$HOME/.cac"
ENVS_DIR="$CAC_DIR/envs"
VERSIONS_DIR="$CAC_DIR/versions"

make_version() {
local ver="$1"
mkdir -p "$VERSIONS_DIR/$ver"
printf '#!/usr/bin/env bash\nexit 0\n' > "$VERSIONS_DIR/$ver/claude"
chmod +x "$VERSIONS_DIR/$ver/claude"
}

make_env() {
local name="$1" ver="$2"
mkdir -p "$ENVS_DIR/$name/.claude"
printf '%s\n' "$ver" > "$ENVS_DIR/$name/version"
}

assert_eq() {
local expected="$1" actual="$2" label="$3"
if [[ "$expected" != "$actual" ]]; then
echo "FAIL: $label: expected '$expected', got '$actual'" >&2
exit 1
fi
}

assert_exists() {
[[ -e "$1" ]] || { echo "FAIL: expected path to exist: $1" >&2; exit 1; }
}

assert_missing() {
[[ ! -e "$1" ]] || { echo "FAIL: expected path to be removed: $1" >&2; exit 1; }
}

strip_ansi() {
perl -pe 's/\e\[[0-9;]*[A-Za-z]//g'
}

mkdir -p "$ENVS_DIR" "$VERSIONS_DIR"
make_version "2.1.100"
make_version "2.1.200"
make_version "2.1.300"
make_env "work" "2.1.100"
make_env "backup" "2.1.100"

UPDATE_OUT="$TMP/cac-update-all.out"
PRUNE_OUT="$TMP/cac-prune.out"

"$CAC" claude update-all 2.1.200 >"$UPDATE_OUT"
assert_eq "2.1.200" "$(tr -d '[:space:]' < "$ENVS_DIR/work/version")" "work version"
assert_eq "2.1.200" "$(tr -d '[:space:]' < "$ENVS_DIR/backup/version")" "backup version"
strip_ansi < "$UPDATE_OUT" | grep -q "Pinned 2 environment(s) -> Claude Code 2.1.200" || {
echo "FAIL: update-all output did not report pinned env count" >&2
cat "$UPDATE_OUT" >&2
exit 1
}

"$CAC" claude prune >"$PRUNE_OUT"
assert_missing "$VERSIONS_DIR/2.1.100"
assert_exists "$VERSIONS_DIR/2.1.200/claude"
assert_missing "$VERSIONS_DIR/2.1.300"
assert_eq "2.1.200" "$(tr -d '[:space:]' < "$VERSIONS_DIR/.latest")" "latest version"
strip_ansi < "$PRUNE_OUT" | grep -q "Uninstalled 2 unused version(s)" || {
echo "FAIL: prune output did not report removed version count" >&2
cat "$PRUNE_OUT" >&2
exit 1
}

echo "PASS: claude bulk version commands"
58 changes: 58 additions & 0 deletions src/cmd_claude.sh
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,60 @@ _claude_cmd_uninstall() {
echo "$(_green_bold "Uninstalled") Claude Code $(_cyan "$ver")"
}

_claude_cmd_update_all() {
local target="${1:-latest}"
local ver
if [[ "$target" == "latest" ]]; then
printf "Fetching latest version ... "
ver=$(_fetch_latest_version) || _die "failed to fetch latest version"
echo "$(_cyan "$ver")"
else
ver="$target"
fi

[[ "$ver" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9._-]+)?$ ]] || \
_die "invalid version $(_cyan "'$ver'")"

mkdir -p "$VERSIONS_DIR"
_download_version "$ver"
_update_latest

local count=0 env_dir
for env_dir in "$ENVS_DIR"/*/; do
[[ -d "$env_dir" ]] || continue
echo "$ver" > "$env_dir/version"
(( count += 1 ))
done

echo "$(_green_bold "Pinned") $count environment(s) -> Claude Code $(_cyan "$ver")"
}

_claude_cmd_prune() {
_update_latest 2>/dev/null || true
if [[ ! -d "$VERSIONS_DIR" ]]; then
echo "$(_dim " No versions installed.")"
return
fi

local removed=0 ver_dir ver count
for ver_dir in "$VERSIONS_DIR"/*/; do
[[ -d "$ver_dir" ]] || continue
ver=$(basename "$ver_dir")
count=$(_envs_using_version "$ver")
if [[ "$count" -eq 0 ]]; then
rm -rf "${VERSIONS_DIR:?}/$ver"
(( removed += 1 ))
fi
done

_update_latest 2>/dev/null || true
if [[ "$removed" -eq 0 ]]; then
echo "$(_dim " No unused versions to prune.")"
else
echo "$(_green_bold "Uninstalled") $removed unused version(s)"
fi
}

_claude_cmd_ls() {
_update_latest 2>/dev/null || true
if [[ ! -d "$VERSIONS_DIR" ]] || [[ -z "$(ls -A "$VERSIONS_DIR" 2>/dev/null)" ]]; then
Expand Down Expand Up @@ -139,13 +193,17 @@ cmd_claude() {
case "${1:-help}" in
install) _claude_cmd_install "${@:2}" ;;
uninstall) _claude_cmd_uninstall "${@:2}" ;;
update-all) _claude_cmd_update_all "${@:2}" ;;
prune) _claude_cmd_prune ;;
ls|list) _claude_cmd_ls ;;
pin) _claude_cmd_pin "${@:2}" ;;
help|-h|--help)
echo "$(_bold "cac claude") — Claude Code version management"
echo
echo " $(_bold "install") [latest|<ver>] Install a Claude Code version"
echo " $(_bold "uninstall") <ver> Remove an installed version"
echo " $(_bold "update-all") [latest|<ver>] Pin all environments to a version"
echo " $(_bold "prune") Remove installed versions not used by any environment"
echo " $(_bold "ls") List installed versions"
echo " $(_bold "pin") <ver> Pin current environment to a version"
;;
Expand Down
2 changes: 2 additions & 0 deletions src/cmd_help.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ cmd_help() {
echo " $(_green "cac claude install") [latest|ver] Install Claude Code"
echo " $(_green "cac claude ls") List installed versions"
echo " $(_green "cac claude pin") <ver> Pin env to a version"
echo " $(_green "cac claude update-all") [latest|ver] Pin all envs to a version"
echo " $(_green "cac claude prune") Remove unused versions"
echo " $(_green "cac claude uninstall") <ver> Remove a version"
echo

Expand Down