claude-cac 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cac CHANGED
@@ -10,7 +10,8 @@ VERSIONS_DIR="$CAC_DIR/versions"
10
10
  # ━━━ utils.sh ━━━
11
11
  # ── utils: 颜色、读写、UUID、proxy 解析 ───────────────────────
12
12
 
13
- CAC_VERSION="1.2.0"
13
+ # shellcheck disable=SC2034 # used in build-concatenated cac script
14
+ CAC_VERSION="1.3.0"
14
15
 
15
16
  _read() { [[ -f "$1" ]] && tr -d '[:space:]' < "$1" || echo "${2:-}"; }
16
17
  _die() { printf '%b\n' "$(_red "error:") $*" >&2; exit 1; }
@@ -68,7 +69,7 @@ _parse_proxy() {
68
69
  host=$(echo "$raw" | cut -d: -f1)
69
70
  port=$(echo "$raw" | cut -d: -f2)
70
71
  user=$(echo "$raw" | cut -d: -f3)
71
- pass=$(echo "$raw" | cut -d: -f4)
72
+ pass=$(echo "$raw" | cut -d: -f4-)
72
73
  if [[ -z "$user" ]]; then
73
74
  echo "http://${host}:${port}"
74
75
  else
@@ -103,7 +104,7 @@ _auto_detect_proxy() {
103
104
  host=$(echo "$raw" | cut -d: -f1)
104
105
  port=$(echo "$raw" | cut -d: -f2)
105
106
  user=$(echo "$raw" | cut -d: -f3)
106
- pass=$(echo "$raw" | cut -d: -f4)
107
+ pass=$(echo "$raw" | cut -d: -f4-)
107
108
  if [[ -n "$user" ]]; then
108
109
  auth_part="${user}:${pass}@"
109
110
  else
@@ -134,6 +135,25 @@ _env_dir() { echo "$ENVS_DIR/$1"; }
134
135
 
135
136
  # ── Version management helpers ────────────────────────────────────
136
137
 
138
+ # Find the highest installed version by semver sort
139
+ _update_latest() {
140
+ local highest=""
141
+ for d in "$VERSIONS_DIR"/*/; do
142
+ [[ -d "$d" ]] || continue
143
+ local v
144
+ v=$(basename "$d")
145
+ [[ "$v" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]] || continue
146
+ if [[ -z "$highest" ]] || [[ "$(printf '%s\n%s\n' "$highest" "$v" | sort -t. -k1,1n -k2,2n -k3,3n | tail -1)" == "$v" ]]; then
147
+ highest="$v"
148
+ fi
149
+ done
150
+ if [[ -n "$highest" ]]; then
151
+ echo "$highest" > "$VERSIONS_DIR/.latest"
152
+ else
153
+ rm -f "$VERSIONS_DIR/.latest"
154
+ fi
155
+ }
156
+
137
157
  _resolve_version() {
138
158
  local v="$1"
139
159
  if [[ "$v" == "latest" || -z "$v" ]]; then
@@ -195,8 +215,8 @@ _ensure_version_installed() {
195
215
  if [[ ! -x "$(_version_binary "$ver")" ]]; then
196
216
  echo "Version $(_cyan "$ver") not installed, downloading ..." >&2
197
217
  mkdir -p "$VERSIONS_DIR"
198
- _download_version "$ver" || return 1
199
- echo "$ver" > "$VERSIONS_DIR/.latest"
218
+ _download_version "$ver" >&2 || return 1
219
+ _update_latest
200
220
  echo >&2
201
221
  fi
202
222
  echo "$ver"
@@ -245,15 +265,22 @@ _find_real_claude() {
245
265
  }
246
266
 
247
267
  _detect_rc_file() {
248
- if [[ -f "$HOME/.zshrc" ]]; then
249
- echo "$HOME/.zshrc"
250
- elif [[ -f "$HOME/.bashrc" ]]; then
251
- echo "$HOME/.bashrc"
252
- elif [[ -f "$HOME/.bash_profile" ]]; then
253
- echo "$HOME/.bash_profile"
254
- else
255
- echo ""
256
- fi
268
+ local shell_name
269
+ shell_name=$(basename "${SHELL:-/bin/bash}")
270
+ case "$shell_name" in
271
+ zsh)
272
+ [[ -f "$HOME/.zshrc" ]] && { echo "$HOME/.zshrc"; return; }
273
+ ;;
274
+ bash)
275
+ [[ -f "$HOME/.bashrc" ]] && { echo "$HOME/.bashrc"; return; }
276
+ [[ -f "$HOME/.bash_profile" ]] && { echo "$HOME/.bash_profile"; return; }
277
+ ;;
278
+ esac
279
+ # Fallback: try common rc files
280
+ [[ -f "$HOME/.bashrc" ]] && { echo "$HOME/.bashrc"; return; }
281
+ [[ -f "$HOME/.zshrc" ]] && { echo "$HOME/.zshrc"; return; }
282
+ [[ -f "$HOME/.bash_profile" ]] && { echo "$HOME/.bash_profile"; return; }
283
+ echo ""
257
284
  }
258
285
 
259
286
  _install_method() {
@@ -292,13 +319,20 @@ _write_path_to_rc() {
292
319
  _remove_path_from_rc "$rc_file"
293
320
  fi
294
321
 
295
- cat >> "$rc_file" << 'EOF'
322
+ cat >> "$rc_file" << 'CACEOF'
296
323
 
297
324
  # >>> cac — Claude Code Cloak >>>
298
- export PATH="$HOME/bin:$PATH" # cac 命令
299
- export PATH="$HOME/.cac/bin:$PATH" # claude wrapper
325
+ PATH=$(echo "$PATH" | tr ':' '\n' | grep -v '\.cac/bin' | grep -v "$HOME/bin" | tr '\n' ':' | sed 's/:$//')
326
+ export PATH="$HOME/.cac/bin:$HOME/bin:$PATH"
327
+ cac() {
328
+ command "$HOME/bin/cac" "$@"
329
+ local _rc=$?
330
+ PATH=$(echo "$PATH" | tr ':' '\n' | grep -v '\.cac/bin' | tr '\n' ':' | sed 's/:$//')
331
+ export PATH="$HOME/.cac/bin:$HOME/bin:$PATH"
332
+ return $_rc
333
+ }
300
334
  # <<< cac — Claude Code Cloak <<<
301
- EOF
335
+ CACEOF
302
336
  echo " ✓ PATH 已写入 $rc_file"
303
337
  return 0
304
338
  }
@@ -333,9 +367,13 @@ _update_statsig() {
333
367
  local config_dir="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
334
368
  local statsig="$config_dir/statsig"
335
369
  [[ -d "$statsig" ]] || return 0
370
+ local found=false
336
371
  for f in "$statsig"/statsig.stable_id.*; do
337
- [[ -f "$f" ]] && printf '"%s"' "$sid" > "$f"
372
+ [[ -f "$f" ]] && { printf '"%s"' "$sid" > "$f"; found=true; }
338
373
  done
374
+ if [[ "$found" == "false" ]]; then
375
+ printf '"%s"' "$sid" > "$statsig/statsig.stable_id.local"
376
+ fi
339
377
  }
340
378
 
341
379
  _update_claude_json_user_id() {
@@ -345,11 +383,16 @@ _update_claude_json_user_id() {
345
383
  [[ -f "$claude_json" ]] || claude_json="$HOME/.claude.json"
346
384
  [[ -f "$claude_json" ]] || return 0
347
385
  python3 - "$claude_json" "$user_id" << 'PYEOF'
348
- import json, sys
386
+ import json, sys, uuid
349
387
  fpath, uid = sys.argv[1], sys.argv[2]
350
388
  with open(fpath) as f:
351
389
  d = json.load(f)
352
390
  d['userID'] = uid
391
+ d['anonymousId'] = 'claudecode.v1.' + str(uuid.uuid4())
392
+ d.pop('numStartups', None)
393
+ d.pop('firstStartTime', None)
394
+ d.pop('cachedGrowthBookFeatures', None)
395
+ d.pop('cachedStatsigGates', None)
353
396
  with open(fpath, 'w') as f:
354
397
  json.dump(d, f, indent=2, ensure_ascii=False)
355
398
  PYEOF
@@ -364,6 +407,7 @@ TELEMETRY_DOMAINS=(
364
407
  "statsig.anthropic.com"
365
408
  "sentry.io"
366
409
  "o1137031.ingest.sentry.io"
410
+ "cdn.growthbook.io"
367
411
  )
368
412
 
369
413
  # 写入 HOSTALIASES 文件(备用层:gethostbyname 级别拦截)
@@ -403,6 +447,7 @@ var BLOCKED_DOMAINS = new Set([
403
447
  'statsig.anthropic.com',
404
448
  'sentry.io',
405
449
  'o1137031.ingest.sentry.io',
450
+ 'cdn.growthbook.io',
406
451
  ]);
407
452
 
408
453
  /**
@@ -877,7 +922,137 @@ _check_mtls() {
877
922
  }
878
923
 
879
924
  # ━━━ templates.sh ━━━
880
- # ── templates: 写入 wrapper 和 ioreg shim ──────────────────────
925
+ # ── templates: 写入 wrappershim、环境初始化 ──────────────────
926
+
927
+ # 写入 statusline-command.sh 到环境 .claude 目录
928
+ _write_statusline_script() {
929
+ local config_dir="$1"
930
+ cat > "$config_dir/statusline-command.sh" << 'STATUSLINE_EOF'
931
+ #!/usr/bin/env bash
932
+ input=$(cat)
933
+
934
+ model=$(echo "$input" | jq -r '.model.display_name // empty')
935
+ cwd=$(echo "$input" | jq -r '.cwd // empty')
936
+ version=$(echo "$input" | jq -r '.version // empty')
937
+ session_name=$(echo "$input" | jq -r '.session_name // empty')
938
+
939
+ used_pct=$(echo "$input" | jq -r '.context_window.used_percentage // empty')
940
+ ctx_size=$(echo "$input" | jq -r '.context_window.context_window_size // empty')
941
+ input_tokens=$(echo "$input" | jq -r '.context_window.current_usage.input_tokens // empty')
942
+
943
+ five_pct=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')
944
+ week_pct=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty')
945
+ five_reset=$(echo "$input" | jq -r '.rate_limits.five_hour.resets_at // empty')
946
+
947
+ worktree_name=$(echo "$input" | jq -r '.worktree.name // empty')
948
+ worktree_branch=$(echo "$input" | jq -r '.worktree.branch // empty')
949
+
950
+ reset='\033[0m'; bold='\033[1m'; dim='\033[2m'
951
+ cyan='\033[36m'; yellow='\033[33m'; green='\033[32m'; red='\033[31m'
952
+ magenta='\033[35m'; blue='\033[34m'
953
+
954
+ parts=()
955
+
956
+ [ -n "$model" ] && parts+=("$(printf "${cyan}${bold}%s${reset}" "$model")")
957
+ [ -n "$version" ] && parts+=("$(printf "${dim}v%s${reset}" "$version")")
958
+ [ -n "$session_name" ] && parts+=("$(printf "${magenta}[%s]${reset}" "$session_name")")
959
+
960
+ if [ -n "$cwd" ]; then
961
+ short_cwd="${cwd/#$HOME/~}"
962
+ parts+=("$(printf "${blue}%s${reset}" "$short_cwd")")
963
+ fi
964
+
965
+ if [ -n "$used_pct" ] && [ -n "$ctx_size" ]; then
966
+ ctx_int=$(printf "%.0f" "$used_pct")
967
+ if [ "$ctx_int" -ge 80 ]; then ctx_color="$red"
968
+ elif [ "$ctx_int" -ge 50 ]; then ctx_color="$yellow"
969
+ else ctx_color="$green"; fi
970
+ ctx_k=$(echo "$ctx_size" | awk '{printf "%.0fk", $1/1000}')
971
+ if [ -n "$input_tokens" ]; then
972
+ tokens_k=$(echo "$input_tokens" | awk '{printf "%.1fk", $1/1000}')
973
+ parts+=("$(printf "${ctx_color}ctx:%.0f%% %s/%s${reset}" "$used_pct" "${tokens_k}" "${ctx_k}")")
974
+ else
975
+ parts+=("$(printf "${ctx_color}ctx:%.0f%% (%s)${reset}" "$used_pct" "${ctx_k}")")
976
+ fi
977
+ fi
978
+
979
+ rate_parts=()
980
+ if [ -n "$five_pct" ]; then
981
+ five_int=$(printf "%.0f" "$five_pct")
982
+ if [ "$five_int" -ge 80 ]; then rc="$red"
983
+ elif [ "$five_int" -ge 50 ]; then rc="$yellow"
984
+ else rc="$green"; fi
985
+ rs="$(printf "${rc}5h:%.0f%%${reset}" "$five_pct")"
986
+ if [ -n "$five_reset" ] && [ "$five_int" -ge 50 ]; then
987
+ rm=$(( (five_reset - $(date +%s)) / 60 ))
988
+ [ "$rm" -gt 0 ] && rs="$rs$(printf "${dim}(${rm}m)${reset}")"
989
+ fi
990
+ rate_parts+=("$rs")
991
+ fi
992
+ if [ -n "$week_pct" ]; then
993
+ week_int=$(printf "%.0f" "$week_pct")
994
+ if [ "$week_int" -ge 80 ]; then wc="$red"
995
+ elif [ "$week_int" -ge 50 ]; then wc="$yellow"
996
+ else wc="$green"; fi
997
+ rate_parts+=("$(printf "${wc}7d:%.0f%%${reset}" "$week_pct")")
998
+ fi
999
+ if [ "${#rate_parts[@]}" -gt 0 ]; then
1000
+ rstr="${rate_parts[0]}"
1001
+ for i in "${rate_parts[@]:1}"; do rstr="$rstr $i"; done
1002
+ parts+=("$rstr")
1003
+ fi
1004
+
1005
+ if [ -n "$worktree_name" ]; then
1006
+ wt="wt:$worktree_name"
1007
+ [ -n "$worktree_branch" ] && wt="$wt($worktree_branch)"
1008
+ parts+=("$(printf "${cyan}%s${reset}" "$wt")")
1009
+ fi
1010
+
1011
+ if [ "${#parts[@]}" -eq 0 ]; then printf "${dim}claude${reset}\n"; exit 0; fi
1012
+ sep="$(printf " ${dim}|${reset} ")"
1013
+ result="${parts[0]}"
1014
+ for p in "${parts[@]:1}"; do result="$result$sep$p"; done
1015
+ printf "%b\n" "$result"
1016
+ STATUSLINE_EOF
1017
+ chmod +x "$config_dir/statusline-command.sh"
1018
+ }
1019
+
1020
+ # 写入 settings.json 到环境 .claude 目录
1021
+ _write_env_settings() {
1022
+ local config_dir="$1"
1023
+ cat > "$config_dir/settings.json" << 'SETTINGS_EOF'
1024
+ {
1025
+ "permissions": {
1026
+ "defaultMode": "bypassPermissions"
1027
+ },
1028
+ "skipDangerousModePermissionPrompt": true,
1029
+ "statusLine": {
1030
+ "type": "command",
1031
+ "command": "bash $CLAUDE_CONFIG_DIR/statusline-command.sh"
1032
+ }
1033
+ }
1034
+ SETTINGS_EOF
1035
+ }
1036
+
1037
+ # 写入 CLAUDE.md 到环境 .claude 目录
1038
+ _write_env_claude_md() {
1039
+ local config_dir="$1"
1040
+ local env_name="$2"
1041
+ cat > "$config_dir/CLAUDE.md" << CLAUDEMD_EOF
1042
+ # cac managed environment
1043
+
1044
+ This Claude Code instance is managed by **cac** (Claude Code Cloak).
1045
+
1046
+ - Environment name: \`$env_name\`
1047
+ - Config directory: \`CLAUDE_CONFIG_DIR\` is set to this \`.claude/\` folder
1048
+ - Your settings, credentials, and sessions are isolated per-environment
1049
+
1050
+ Useful commands:
1051
+ - \`cac env ls\` — list all environments and their config paths
1052
+ - \`cac env check\` — verify current environment health
1053
+ - \`cac <name>\` — switch to another environment
1054
+ CLAUDEMD_EOF
1055
+ }
881
1056
 
882
1057
  _write_wrapper() {
883
1058
  mkdir -p "$CAC_DIR/bin"
@@ -906,6 +1081,8 @@ _env_dir="$ENVS_DIR/$_name"
906
1081
  # Isolated .claude config directory
907
1082
  if [[ -d "$_env_dir/.claude" ]]; then
908
1083
  export CLAUDE_CONFIG_DIR="$_env_dir/.claude"
1084
+ # 确保 settings.json 存在,阻止 Claude Code fallback 到 ~/.claude/settings.json
1085
+ [[ -f "$_env_dir/.claude/settings.json" ]] || echo '{}' > "$_env_dir/.claude/settings.json"
909
1086
  fi
910
1087
 
911
1088
  # Proxy — optional: only if proxy file exists and is non-empty
@@ -930,9 +1107,13 @@ fi
930
1107
  if [[ -f "$_env_dir/stable_id" ]]; then
931
1108
  _sid=$(tr -d '[:space:]' < "$_env_dir/stable_id")
932
1109
  _config_dir="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
933
- for _f in "$_config_dir/statsig"/statsig.stable_id.*; do
934
- [[ -f "$_f" ]] && printf '"%s"' "$_sid" > "$_f"
935
- done
1110
+ if [[ -d "$_config_dir/statsig" ]]; then
1111
+ _sid_found=false
1112
+ for _f in "$_config_dir/statsig"/statsig.stable_id.*; do
1113
+ [[ -f "$_f" ]] && { printf '"%s"' "$_sid" > "$_f"; _sid_found=true; }
1114
+ done
1115
+ [[ "$_sid_found" == "false" ]] && printf '"%s"' "$_sid" > "$_config_dir/statsig/statsig.stable_id.local"
1116
+ fi
936
1117
  fi
937
1118
 
938
1119
  # 注入环境变量 —— 代理(仅在配置了代理时)
@@ -1214,6 +1395,13 @@ _ensure_initialized() {
1214
1395
  _write_dns_guard_js 2>/dev/null || true
1215
1396
  _write_blocked_hosts 2>/dev/null || true
1216
1397
 
1398
+ # PATH (idempotent — always ensure it's in rc file)
1399
+ local rc_file; rc_file=$(_detect_rc_file)
1400
+ _write_path_to_rc "$rc_file" >/dev/null 2>&1 || true
1401
+
1402
+ # Keep .latest pointing to highest installed version
1403
+ _update_latest 2>/dev/null || true
1404
+
1217
1405
  # Rest only needed on first init
1218
1406
  [[ -f "$CAC_DIR/bin/claude" ]] && return 0
1219
1407
 
@@ -1244,10 +1432,6 @@ _ensure_initialized() {
1244
1432
 
1245
1433
  # mTLS CA
1246
1434
  _generate_ca_cert 2>/dev/null || true
1247
-
1248
- # PATH (idempotent)
1249
- local rc_file; rc_file=$(_detect_rc_file)
1250
- _write_path_to_rc "$rc_file" >/dev/null 2>&1 || true
1251
1435
  }
1252
1436
 
1253
1437
  # Explicit setup command — runs initialization with verbose output
@@ -1305,7 +1489,7 @@ _env_cmd_create() {
1305
1489
  esac
1306
1490
  done
1307
1491
 
1308
- [[ -n "$name" ]] || _die "usage: cac env create <name> [-p <proxy>] [-c <version>] [--type local|container]"
1492
+ [[ -n "$name" ]] || _die "usage: cac env create <name> [-p <proxy>] [-c <version>]"
1309
1493
  [[ "$name" =~ ^[a-zA-Z0-9_-]+$ ]] || _die "invalid name '$name' (use alphanumeric, dash, underscore)"
1310
1494
 
1311
1495
  local env_dir="$ENVS_DIR/$name"
@@ -1314,15 +1498,15 @@ _env_cmd_create() {
1314
1498
  _timer_start
1315
1499
 
1316
1500
  # Auto-install version (just-in-time, like uv)
1317
- if [[ -n "$claude_ver" ]]; then
1318
- claude_ver=$(_ensure_version_installed "$claude_ver") || exit 1
1319
- fi
1501
+ # No version specified use latest
1502
+ [[ -z "$claude_ver" ]] && claude_ver="latest"
1503
+ claude_ver=$(_ensure_version_installed "$claude_ver") || exit 1
1320
1504
 
1321
1505
  # Auto-detect proxy protocol
1322
1506
  local proxy_url=""
1323
1507
  if [[ -n "$proxy" ]]; then
1324
1508
  if [[ ! "$proxy" =~ ^(http|https|socks5):// ]]; then
1325
- printf "Detecting proxy protocol ... "
1509
+ printf " $(_dim "Detecting proxy protocol ...") "
1326
1510
  if proxy_url=$(_auto_detect_proxy "$proxy"); then
1327
1511
  echo "$(_cyan "$(echo "$proxy_url" | grep -oE '^[a-z]+')")"
1328
1512
  else
@@ -1336,7 +1520,7 @@ _env_cmd_create() {
1336
1520
  # Geo-detect timezone (single request via proxy)
1337
1521
  local tz="America/New_York" lang="en_US.UTF-8"
1338
1522
  if [[ -n "$proxy_url" ]]; then
1339
- printf "Detecting timezone ... "
1523
+ printf " $(_dim "Detecting timezone ...") "
1340
1524
  local ip_info
1341
1525
  ip_info=$(curl -s --proxy "$proxy_url" --connect-timeout 8 "http://ip-api.com/json/?fields=timezone,countryCode" 2>/dev/null || true)
1342
1526
  if [[ -n "$ip_info" ]]; then
@@ -1363,42 +1547,86 @@ _env_cmd_create() {
1363
1547
  echo "$env_type" > "$env_dir/type"
1364
1548
  mkdir -p "$env_dir/.claude"
1365
1549
 
1550
+ # Initialize settings.json, statusline, and CLAUDE.md
1551
+ _write_env_settings "$env_dir/.claude"
1552
+ _write_statusline_script "$env_dir/.claude"
1553
+ _write_env_claude_md "$env_dir/.claude" "$name"
1554
+
1366
1555
  _generate_client_cert "$name" >/dev/null 2>&1 || true
1367
1556
 
1557
+ # Auto-activate
1558
+ echo "$name" > "$CAC_DIR/current"
1559
+ rm -f "$CAC_DIR/stopped"
1560
+ if [[ -d "$env_dir/.claude" ]]; then
1561
+ export CLAUDE_CONFIG_DIR="$env_dir/.claude"
1562
+ fi
1563
+ _update_statsig "$(_read "$env_dir/stable_id")" 2>/dev/null || true
1564
+ _update_claude_json_user_id "$(_read "$env_dir/user_id")" 2>/dev/null || true
1565
+
1368
1566
  local elapsed; elapsed=$(_timer_elapsed)
1369
- echo "$(_green_bold "Created") environment $(_cyan "$name") $(_dim "in $elapsed")"
1370
- [[ -n "$proxy_url" ]] && echo " $(_green "+") proxy: $proxy_url"
1371
- [[ -n "$claude_ver" ]] && echo " $(_green "+") claude: $(_cyan "$claude_ver")"
1372
- echo " $(_green "+") type: $env_type"
1373
1567
  echo
1374
- echo "Activate with: $(_green "cac $name")"
1568
+ echo " $(_green_bold "Created") $(_bold "$name") $(_dim "in $elapsed")"
1569
+ echo
1570
+ [[ -n "$proxy_url" ]] && echo " $(_green "+") proxy $proxy_url"
1571
+ [[ -n "$claude_ver" ]] && echo " $(_green "+") claude $(_cyan "$claude_ver")"
1572
+ echo " $(_green "+") env $(_dim "${env_dir/#$HOME/~}/.claude/")"
1573
+ echo
1574
+ echo " $(_dim "Environment activated. Run") $(_green "claude") $(_dim "to start.")"
1575
+ echo
1375
1576
  }
1376
1577
 
1377
1578
  _env_cmd_ls() {
1378
1579
  if [[ ! -d "$ENVS_DIR" ]] || [[ -z "$(ls -A "$ENVS_DIR" 2>/dev/null)" ]]; then
1379
- echo "$(_dim "(no environments — create with 'cac env create <name>')")"
1580
+ echo "$(_dim " No environments yet.")"
1581
+ echo " Run $(_green "cac env create <name>") to get started."
1380
1582
  return
1381
1583
  fi
1382
1584
 
1383
1585
  local current; current=$(_current_env)
1384
- local stopped_tag=""
1385
- [[ -f "$CAC_DIR/stopped" ]] && stopped_tag=" $(_red "[stopped]")"
1386
1586
 
1587
+ # Collect data first to calculate column widths
1588
+ local names=() versions=() proxies=() paths=()
1387
1589
  for env_dir in "$ENVS_DIR"/*/; do
1388
1590
  [[ -d "$env_dir" ]] || continue
1389
- local name; name=$(basename "$env_dir")
1390
- local proxy; proxy=$(_read "$env_dir/proxy" "")
1391
- local ver; ver=$(_read "$env_dir/version" "system")
1392
- local etype; etype=$(_read "$env_dir/type" "local")
1591
+ names+=("$(basename "$env_dir")")
1592
+ versions+=("$(_read "$env_dir/version" "system")")
1593
+ local p; p=$(_read "$env_dir/proxy" "")
1594
+ if [[ -n "$p" ]] && [[ "$p" == *"://"*"@"* ]]; then
1595
+ p=$(echo "$p" | sed 's|://[^@]*@|://***@|')
1596
+ fi
1597
+ proxies+=("${p:-—}")
1598
+ local ep="${env_dir}.claude/"
1599
+ paths+=("${ep/#$HOME/~}")
1600
+ done
1601
+
1602
+ # Calculate max widths
1603
+ local max_name=4 max_ver=6 max_proxy=5
1604
+ local i
1605
+ for i in "${!names[@]}"; do
1606
+ local nl=${#names[$i]} vl=${#versions[$i]} pl=${#proxies[$i]}
1607
+ (( nl > max_name )) && max_name=$nl
1608
+ (( vl > max_ver )) && max_ver=$vl
1609
+ (( pl > max_proxy )) && max_proxy=$pl
1610
+ done
1611
+ # Cap proxy column
1612
+ (( max_proxy > 40 )) && max_proxy=40
1613
+
1614
+ # Header
1615
+ printf " $(_dim " %-${max_name}s %-${max_ver}s %-${max_proxy}s %s")" "NAME" "CLAUDE" "PROXY" "ENV"
1616
+ echo
1617
+
1618
+ # Rows
1619
+ for i in "${!names[@]}"; do
1620
+ local name="${names[$i]}"
1621
+ local ver="${versions[$i]}"
1622
+ local proxy="${proxies[$i]}"
1623
+ local epath="${paths[$i]}"
1393
1624
 
1394
1625
  if [[ "$name" == "$current" ]]; then
1395
- printf " %s %s%s\n" "$(_green "")" "$(_bold "$name")" "$stopped_tag"
1626
+ printf " $(_green "▶") $(_bold "%-${max_name}s") $(_cyan "%-${max_ver}s") %-${max_proxy}s $(_dim "%s")\n" "$name" "$ver" "$proxy" "$epath"
1396
1627
  else
1397
- printf " %s\n" "$name"
1628
+ printf " $(_dim "○") %-${max_name}s $(_cyan "%-${max_ver}s") $(_dim "%-${max_proxy}s") $(_dim "%s")\n" "$name" "$ver" "$proxy" "$epath"
1398
1629
  fi
1399
- local details="claude: $(_cyan "$ver") type: $etype"
1400
- [[ -n "$proxy" ]] && details="proxy: $proxy $details"
1401
- printf " %s\n" "$details"
1402
1630
  done
1403
1631
  }
1404
1632
 
@@ -1410,7 +1638,7 @@ _env_cmd_rm() {
1410
1638
  local current; current=$(_current_env)
1411
1639
  [[ "$name" != "$current" ]] || _die "cannot remove active environment $(_cyan "'$name'")\n switch to another environment first"
1412
1640
 
1413
- rm -rf "$ENVS_DIR/$name"
1641
+ rm -rf "${ENVS_DIR:?}/$name"
1414
1642
  echo "$(_green_bold "Removed") environment $(_cyan "$name")"
1415
1643
  }
1416
1644
 
@@ -1444,37 +1672,115 @@ _env_cmd_activate() {
1444
1672
  echo "$(_green_bold "Activated") $(_bold "$name") $(_dim "in $elapsed")"
1445
1673
  }
1446
1674
 
1447
- _env_cmd_deactivate() {
1448
- if [[ ! -f "$CAC_DIR/current" ]]; then
1449
- echo "$(_dim "no active environment")"
1675
+ _env_cmd_set() {
1676
+ _require_setup
1677
+
1678
+ # Parse: cac env set [name] <key> <value|--remove>
1679
+ # If first arg is a known key, use current env; otherwise treat as env name
1680
+ local name="" key="" value="" remove=false
1681
+ local known_keys="proxy version"
1682
+
1683
+ if [[ $# -lt 1 ]] || [[ "${1:-}" == "-h" ]] || [[ "${1:-}" == "--help" ]] || [[ "${1:-}" == "help" ]]; then
1684
+ echo
1685
+ echo " $(_bold "cac env set") — modify environment configuration"
1686
+ echo
1687
+ echo " $(_green "set") [name] proxy <url> Set proxy"
1688
+ echo " $(_green "set") [name] proxy --remove Remove proxy"
1689
+ echo " $(_green "set") [name] version <ver|latest> Change Claude version"
1690
+ echo
1691
+ echo " $(_dim "If name is omitted, uses the current active environment.")"
1692
+ echo
1450
1693
  return
1451
1694
  fi
1452
- local current; current=$(_current_env)
1453
- rm -f "$CAC_DIR/current"
1454
- touch "$CAC_DIR/stopped"
1455
- _relay_stop 2>/dev/null || true
1456
- echo "$(_green_bold "Deactivated") $(_bold "$current")claude runs unprotected"
1695
+
1696
+ # Is first arg a known key or an env name?
1697
+ if echo "$known_keys" | grep -qw "${1:-}"; then
1698
+ name=$(_current_env)
1699
+ [[ -n "$name" ]] || _die "no active environment specify env name"
1700
+ else
1701
+ name="$1"; shift
1702
+ fi
1703
+
1704
+ _require_env "$name"
1705
+ local env_dir="$ENVS_DIR/$name"
1706
+
1707
+ [[ $# -ge 1 ]] || _die "usage: cac env set [name] <proxy|version|bypass> <value|--remove>"
1708
+ key="$1"; shift
1709
+
1710
+ # Parse value or --remove
1711
+ if [[ "${1:-}" == "--remove" ]]; then
1712
+ remove=true; shift
1713
+ elif [[ $# -ge 1 ]]; then
1714
+ value="$1"; shift
1715
+ fi
1716
+
1717
+ case "$key" in
1718
+ proxy)
1719
+ if [[ "$remove" == "true" ]]; then
1720
+ rm -f "$env_dir/proxy"
1721
+ echo "$(_green_bold "Removed") proxy from $(_bold "$name")"
1722
+ else
1723
+ [[ -n "$value" ]] || _die "usage: cac env set [name] proxy <url|host:port:user:pass>"
1724
+ local proxy_url
1725
+ if [[ ! "$value" =~ ^(http|https|socks5):// ]]; then
1726
+ printf " $(_dim "Detecting proxy protocol ...") "
1727
+ if proxy_url=$(_auto_detect_proxy "$value"); then
1728
+ echo "$(_cyan "$(echo "$proxy_url" | grep -oE '^[a-z]+')")"
1729
+ else
1730
+ echo "$(_yellow "failed, defaulting to http")"
1731
+ fi
1732
+ else
1733
+ proxy_url=$(_parse_proxy "$value")
1734
+ fi
1735
+ echo "$proxy_url" > "$env_dir/proxy"
1736
+ echo "$(_green_bold "Set") proxy for $(_bold "$name") → $proxy_url"
1737
+ fi
1738
+ ;;
1739
+ version)
1740
+ [[ "$remove" != "true" ]] || _die "cannot remove version — use 'cac env set $name version latest'"
1741
+ [[ -n "$value" ]] || _die "usage: cac env set [name] version <ver|latest>"
1742
+ local ver
1743
+ ver=$(_ensure_version_installed "$value") || exit 1
1744
+ echo "$ver" > "$env_dir/version"
1745
+ echo "$(_green_bold "Set") version for $(_bold "$name") → $(_cyan "$ver")"
1746
+ ;;
1747
+ *)
1748
+ _die "unknown key '$key' — use proxy or version"
1749
+ ;;
1750
+ esac
1457
1751
  }
1458
1752
 
1459
1753
  cmd_env() {
1460
1754
  case "${1:-help}" in
1461
1755
  create) _env_cmd_create "${@:2}" ;;
1756
+ set) _env_cmd_set "${@:2}" ;;
1462
1757
  ls|list) _env_cmd_ls ;;
1463
1758
  rm|remove) _env_cmd_rm "${@:2}" ;;
1464
1759
  activate) _env_cmd_activate "${@:2}" ;;
1465
- deactivate) _env_cmd_deactivate ;;
1466
- check) cmd_check ;;
1760
+ check) cmd_check "${@:2}" ;;
1761
+ deactivate) echo "$(_yellow "warning:") deactivate has been removed — switch with 'cac <name>' or uninstall with 'cac self delete'" >&2 ;;
1467
1762
  help|-h|--help)
1468
- echo "$(_bold "cac env") — environment management"
1469
1763
  echo
1470
- echo " $(_bold "create") <name> [-p <proxy>] [-c <ver>] [--type local|container]"
1471
- echo " $(_bold "ls") List all environments"
1472
- echo " $(_bold "rm") <name> Remove an environment"
1473
- echo " $(_bold "activate") <name> Activate (shortcut: cac <name>)"
1474
- echo " $(_bold "deactivate") Deactivate claude runs unprotected"
1475
- echo " $(_bold "check") Verify current environment"
1764
+ echo " $(_bold "cac env") environment management"
1765
+ echo
1766
+ echo " $(_green "create") <name> [-p proxy] [-c ver]"
1767
+ echo " $(_green "set") [name] proxy <url> Set proxy"
1768
+ echo " $(_green "set") [name] proxy --remove Remove proxy"
1769
+ echo " $(_green "set") [name] version <ver|latest> Change Claude version"
1770
+ echo " $(_green "ls") List all environments"
1771
+ echo " $(_green "rm") <name> Remove an environment"
1772
+ echo " $(_green "check") Verify current environment"
1773
+ echo " $(_green "cac") <name> Switch environment"
1774
+ echo
1775
+ ;;
1776
+ *)
1777
+ # If first arg is an existing env name, treat as: cac env set <name> ...
1778
+ if [[ -d "$ENVS_DIR/$1" ]] && [[ $# -ge 2 ]]; then
1779
+ _env_cmd_set "$@"
1780
+ else
1781
+ _die "unknown: cac env $1"
1782
+ fi
1476
1783
  ;;
1477
- *) _die "unknown: cac env $1" ;;
1478
1784
  esac
1479
1785
  }
1480
1786
 
@@ -1505,8 +1811,8 @@ _relay_start() {
1505
1811
  disown
1506
1812
 
1507
1813
  # 等待 relay 就绪
1508
- local i
1509
- for i in {1..30}; do
1814
+ local _i
1815
+ for _i in {1..30}; do
1510
1816
  (echo >/dev/tcp/127.0.0.1/$port) 2>/dev/null && break
1511
1817
  sleep 0.1
1512
1818
  done
@@ -1527,8 +1833,8 @@ _relay_stop() {
1527
1833
  if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
1528
1834
  kill "$pid" 2>/dev/null
1529
1835
  # 等待进程退出
1530
- local i
1531
- for i in {1..20}; do
1836
+ local _i
1837
+ for _i in {1..20}; do
1532
1838
  kill -0 "$pid" 2>/dev/null || break
1533
1839
  sleep 0.1
1534
1840
  done
@@ -1702,18 +2008,14 @@ cmd_check() {
1702
2008
 
1703
2009
  local current; current=$(_current_env)
1704
2010
 
1705
- if [[ -f "$CAC_DIR/stopped" ]]; then
1706
- echo "$(_red "✗") cac 已停用 — 运行 'cac <name>' 恢复"
1707
- return
1708
- fi
1709
2011
  if [[ -z "$current" ]]; then
1710
- echo "错误:未激活任何环境,运行 'cac <name>'" >&2; exit 1
2012
+ echo "error: no active environment — run $(_green "cac env create <name>")" >&2; exit 1
1711
2013
  fi
1712
2014
 
1713
2015
  local env_dir="$ENVS_DIR/$current"
1714
2016
  local proxy; proxy=$(_read "$env_dir/proxy" "")
1715
2017
 
1716
- # 解析版本号
2018
+ # Resolve version
1717
2019
  local ver; ver=$(_read "$env_dir/version" "")
1718
2020
  if [[ -z "$ver" ]] || [[ "$ver" == "system" ]]; then
1719
2021
  local _real; _real=$(_read "$CAC_DIR/real_claude" "")
@@ -1725,80 +2027,27 @@ cmd_check() {
1725
2027
  fi
1726
2028
 
1727
2029
  local problems=()
1728
- local summary_parts=()
1729
-
1730
- # ── wrapper 检查 ──
1731
- local claude_path; claude_path="$(command -v claude 2>/dev/null || true)"
1732
- if [[ -z "$claude_path" ]] || [[ "$(readlink -f "$claude_path" 2>/dev/null || echo "$claude_path")" != *"/.cac/bin/claude"* ]]; then
1733
- if [[ "$claude_path" != *"/.cac/bin/claude" ]]; then
1734
- problems+=("claude 未指向 wrapper — 运行 source ~/.zshrc 或重开终端")
1735
- fi
1736
- fi
1737
2030
 
1738
- # ── 网络检查 ──
1739
- local proxy_ip=""
1740
- if [[ -n "$proxy" ]]; then
1741
- if ! _proxy_reachable "$proxy"; then
1742
- problems+=("代理不通: $proxy")
1743
- else
1744
- proxy_ip=$(curl -s --proxy "$proxy" --connect-timeout 8 https://api.ipify.org 2>/dev/null || true)
1745
- if [[ -n "$proxy_ip" ]]; then
1746
- summary_parts+=("出口 $proxy_ip")
1747
- else
1748
- problems+=("出口 IP 获取失败")
1749
- fi
1750
- fi
2031
+ # ── header (neutral, no pass/fail yet) ──
2032
+ echo
2033
+ echo " $(_bold "$current") $(_dim "(claude $ver)")"
2034
+ echo
1751
2035
 
1752
- # 冲突检测(仅代理连通时才有意义)
1753
- if [[ -n "$proxy_ip" ]]; then
1754
- local os; os=$(_detect_os)
1755
- local has_conflict=false
1756
- local tun_procs="clash|mihomo|sing-box|surge|shadowrocket|v2ray|xray|hysteria|tuic|nekoray"
1757
- local running
1758
- if [[ "$os" == "macos" ]]; then
1759
- running=$(ps aux 2>/dev/null | grep -iE "$tun_procs" | grep -v grep || true)
2036
+ # ── wrapper check (instant) ──
2037
+ local claude_path; claude_path="$(command -v claude 2>/dev/null || true)"
2038
+ if [[ -z "$claude_path" ]] || [[ "$claude_path" != *"/.cac/bin/claude" ]]; then
2039
+ local _rc; _rc=$(_detect_rc_file)
2040
+ if [[ -n "$_rc" ]] && grep -q '# >>> cac' "$_rc" 2>/dev/null; then
2041
+ echo " $(_green "✓") wrapper configured in ${_rc/#$HOME/~}"
1760
2042
  else
1761
- running=$(ps -eo comm 2>/dev/null | grep -iE "$tun_procs" || true)
1762
- fi
1763
- [[ -n "$running" ]] && has_conflict=true
1764
- if [[ "$os" == "macos" ]]; then
1765
- local tun_count; tun_count=$(ifconfig 2>/dev/null | grep -cE '^utun[0-9]+' || echo 0)
1766
- [[ "$tun_count" -gt 3 ]] && has_conflict=true
1767
- elif [[ "$os" == "linux" ]]; then
1768
- ip link show tun0 >/dev/null 2>&1 && has_conflict=true
1769
- fi
1770
-
1771
- if [[ "$has_conflict" == "true" ]]; then
1772
- # 测试 relay
1773
- local relay_ok=false
1774
- if _relay_is_running 2>/dev/null; then
1775
- local rport; rport=$(_read "$CAC_DIR/relay.port" "")
1776
- local relay_ip; relay_ip=$(curl -s --proxy "http://127.0.0.1:$rport" --connect-timeout 8 https://api.ipify.org 2>/dev/null || true)
1777
- [[ -n "$relay_ip" ]] && relay_ok=true
1778
- elif [[ -f "$CAC_DIR/relay.js" ]]; then
1779
- local _test_env; _test_env=$(_current_env)
1780
- if _relay_start "$_test_env" 2>/dev/null; then
1781
- local rport; rport=$(_read "$CAC_DIR/relay.port" "")
1782
- local relay_ip; relay_ip=$(curl -s --proxy "http://127.0.0.1:$rport" --connect-timeout 8 https://api.ipify.org 2>/dev/null || true)
1783
- _relay_stop 2>/dev/null || true
1784
- [[ -n "$relay_ip" ]] && relay_ok=true
1785
- fi
1786
- fi
1787
-
1788
- if [[ "$relay_ok" == "true" ]]; then
1789
- summary_parts+=("TUN 冲突已绕过")
1790
- else
1791
- local proxy_hp; proxy_hp=$(_proxy_host_port "$proxy")
1792
- local proxy_host="${proxy_hp%%:*}"
1793
- problems+=("代理冲突:需在代理软件中为 $proxy_host 添加 DIRECT 规则")
1794
- fi
1795
- fi
2043
+ _write_path_to_rc "$_rc" >/dev/null 2>&1 || true
2044
+ echo " $(_green "✓") wrapper $(_dim "added to ${_rc/#$HOME/~}")"
1796
2045
  fi
1797
2046
  else
1798
- summary_parts+=("API Key 模式")
2047
+ echo " $(_green "✓") wrapper active"
1799
2048
  fi
1800
2049
 
1801
- # ── 防护检查 ──
2050
+ # ── telemetry shield (instant) ──
1802
2051
  local wrapper_file="$CAC_DIR/bin/claude"
1803
2052
  local wrapper_content=""
1804
2053
  [[ -f "$wrapper_file" ]] && wrapper_content=$(<"$wrapper_file")
@@ -1814,43 +2063,125 @@ cmd_check() {
1814
2063
  done
1815
2064
 
1816
2065
  if [[ "$env_ok" -eq "$env_total" ]]; then
1817
- summary_parts+=("遥测屏蔽 ${env_ok}/${env_total}")
2066
+ echo " $(_green "✓") telemetry ${env_ok}/${env_total} blocked"
1818
2067
  else
1819
- problems+=("遥测屏蔽 ${env_ok}/${env_total}")
2068
+ echo " $(_red "✗") telemetry ${env_ok}/${env_total} blocked"
2069
+ problems+=("telemetry shield ${env_ok}/${env_total}")
1820
2070
  fi
1821
2071
 
1822
- # ── 输出结论 ──
2072
+ # ── network check (slow — streaming output) ──
2073
+ local proxy_ip=""
2074
+ if [[ -n "$proxy" ]]; then
2075
+ if ! _proxy_reachable "$proxy"; then
2076
+ echo " $(_red "✗") proxy unreachable"
2077
+ problems+=("proxy unreachable: $proxy")
2078
+ else
2079
+ # Fast retry with dots: each attempt adds a dot
2080
+ local _ip_url _dots=""
2081
+ local _urls="https://api.ip.sb/ip https://ip.3322.net https://api.ipify.org https://ipinfo.io/ip https://api.ip.sb/ip"
2082
+ for _ip_url in $_urls; do
2083
+ _dots="${_dots}."
2084
+ printf "\r · exit IP $(_dim "detecting${_dots}")"
2085
+ proxy_ip=$(curl --proxy "$proxy" --connect-timeout 3 --max-time 6 "$_ip_url" 2>/dev/null || true)
2086
+ [[ "$proxy_ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] && break
2087
+ proxy_ip=""
2088
+ done
2089
+ # Overwrite the "detecting..." line
2090
+ if [[ -n "$proxy_ip" ]]; then
2091
+ printf "\r $(_green "✓") exit IP $(_cyan "$proxy_ip")\033[K\n"
2092
+ else
2093
+ printf "\r $(_green "✓") exit IP $(_dim "run again to detect exit IP")\033[K\n"
2094
+ fi
2095
+
2096
+ # TUN conflict detection
2097
+ if [[ -n "$proxy_ip" ]]; then
2098
+ local os; os=$(_detect_os)
2099
+ local has_conflict=false
2100
+ local tun_procs="clash|mihomo|sing-box|surge|shadowrocket|v2ray|xray|hysteria|tuic|nekoray"
2101
+ local running
2102
+ if [[ "$os" == "macos" ]]; then
2103
+ running=$(ps aux 2>/dev/null | grep -iE "$tun_procs" | grep -v grep || true)
2104
+ else
2105
+ running=$(ps -eo comm 2>/dev/null | grep -iE "$tun_procs" || true)
2106
+ fi
2107
+ [[ -n "$running" ]] && has_conflict=true
2108
+ if [[ "$os" == "macos" ]]; then
2109
+ local tun_count; tun_count=$(ifconfig 2>/dev/null | grep -cE '^utun[0-9]+' || echo 0)
2110
+ [[ "$tun_count" -gt 3 ]] && has_conflict=true
2111
+ elif [[ "$os" == "linux" ]]; then
2112
+ ip link show tun0 >/dev/null 2>&1 && has_conflict=true
2113
+ fi
2114
+
2115
+ if [[ "$has_conflict" == "true" ]]; then
2116
+ local relay_ok=false
2117
+ if _relay_is_running 2>/dev/null; then
2118
+ local rport; rport=$(_read "$CAC_DIR/relay.port" "")
2119
+ local relay_ip; relay_ip=$(curl --proxy "http://127.0.0.1:$rport" --connect-timeout 8 --max-time 12 https://api.ipify.org 2>/dev/null || true)
2120
+ [[ -n "$relay_ip" ]] && relay_ok=true
2121
+ elif [[ -f "$CAC_DIR/relay.js" ]]; then
2122
+ local _test_env; _test_env=$(_current_env)
2123
+ if _relay_start "$_test_env" 2>/dev/null; then
2124
+ local rport; rport=$(_read "$CAC_DIR/relay.port" "")
2125
+ local relay_ip; relay_ip=$(curl --proxy "http://127.0.0.1:$rport" --connect-timeout 8 --max-time 12 https://api.ipify.org 2>/dev/null || true)
2126
+ _relay_stop 2>/dev/null || true
2127
+ [[ -n "$relay_ip" ]] && relay_ok=true
2128
+ fi
2129
+ fi
2130
+
2131
+ if [[ "$relay_ok" == "true" ]]; then
2132
+ echo " $(_green "✓") TUN relay bypass active"
2133
+ else
2134
+ local proxy_hp; proxy_hp=$(_proxy_host_port "$proxy")
2135
+ local proxy_host="${proxy_hp%%:*}"
2136
+ echo " $(_red "✗") TUN conflict — add DIRECT rule for $proxy_host"
2137
+ problems+=("TUN conflict: add DIRECT rule for $proxy_host in proxy software")
2138
+ fi
2139
+ fi
2140
+ fi
2141
+ fi
2142
+ else
2143
+ echo " $(_green "✓") mode API Key (no proxy)"
2144
+ fi
2145
+
2146
+ # ── summary ──
2147
+ echo
1823
2148
  if [[ ${#problems[@]} -eq 0 ]]; then
1824
- echo "$(_green "✓") $(_bold "$current") (claude $(_cyan "$ver")) — 一切正常"
1825
- echo " $(IFS=' | '; echo "${summary_parts[*]}")"
2149
+ echo " $(_green "✓") all good"
1826
2150
  else
1827
- echo "$(_red "✗") $(_bold "$current") (claude $(_cyan "$ver")) — 发现 ${#problems[@]} 个问题"
1828
2151
  for p in "${problems[@]}"; do
1829
2152
  echo " $(_red "✗") $p"
1830
2153
  done
1831
2154
  fi
2155
+ echo
1832
2156
 
1833
- # ── 详细模式 ──
2157
+ # ── verbose mode ──
1834
2158
  if [[ "$verbose" == "true" ]]; then
2159
+ echo " $(_bold "Details")"
2160
+ echo " $(_dim "UUID") $(_read "$env_dir/uuid")"
2161
+ echo " $(_dim "stable_id") $(_read "$env_dir/stable_id")"
2162
+ echo " $(_dim "user_id") $(_read "$env_dir/user_id" "—")"
2163
+ echo " $(_dim "TZ") $(_read "$env_dir/tz" "—")"
2164
+ echo " $(_dim "LANG") $(_read "$env_dir/lang" "—")"
2165
+ echo " $(_dim "env") ${env_dir/#$HOME/~}/.claude/"
1835
2166
  echo
1836
- echo " UUID : $(_read "$env_dir/uuid")"
1837
- echo " stable_id : $(_read "$env_dir/stable_id")"
1838
- echo " user_id : $(_read "$env_dir/user_id" "—")"
1839
- echo " TZ : $(_read "$env_dir/tz" "—")"
1840
- echo " LANG : $(_read "$env_dir/lang" "—")"
1841
- echo " 遥测屏蔽 : ${env_ok}/${env_total}"
2167
+ echo " $(_bold "Telemetry") ${env_ok}/${env_total}"
1842
2168
  for var in "${env_vars[@]}"; do
1843
- printf " %-36s" "$var"
1844
- [[ "$wrapper_content" == *"$var"* ]] && echo "$(_green "✓")" || echo "$(_red "✗")"
2169
+ if [[ "$wrapper_content" == *"$var"* ]]; then
2170
+ printf " $(_green "✓") %s\n" "$var"
2171
+ else
2172
+ printf " $(_red "✗") %s\n" "$var"
2173
+ fi
1845
2174
  done
1846
- printf " DNS 拦截 : "
2175
+ echo
2176
+ printf " $(_bold "DNS block") "
1847
2177
  if [[ -f "$CAC_DIR/cac-dns-guard.js" ]]; then
1848
2178
  _check_dns_block "statsig.anthropic.com"
1849
2179
  else
1850
2180
  echo "$(_red "✗")"
1851
2181
  fi
1852
- printf " mTLS : "
2182
+ printf " $(_bold "mTLS") "
1853
2183
  _check_mtls "$env_dir"
2184
+ echo
1854
2185
  fi
1855
2186
  }
1856
2187
 
@@ -1959,7 +2290,7 @@ _claude_cmd_install() {
1959
2290
 
1960
2291
  mkdir -p "$VERSIONS_DIR"
1961
2292
  if _download_version "$ver"; then
1962
- echo "$ver" > "$VERSIONS_DIR/.latest"
2293
+ _update_latest
1963
2294
  echo
1964
2295
  echo " Bind to environment: $(_cyan "cac env create <name> -c $ver")"
1965
2296
  fi
@@ -1973,35 +2304,34 @@ _claude_cmd_uninstall() {
1973
2304
  local count; count=$(_envs_using_version "$ver")
1974
2305
  [[ "$count" -eq 0 ]] || _die "version $(_cyan "$ver") in use by $count environment(s)"
1975
2306
 
1976
- rm -rf "$VERSIONS_DIR/$ver"
2307
+ rm -rf "${VERSIONS_DIR:?}/$ver"
2308
+ _update_latest
1977
2309
  echo "$(_green_bold "Uninstalled") Claude Code $(_cyan "$ver")"
1978
2310
  }
1979
2311
 
1980
2312
  _claude_cmd_ls() {
1981
- echo "$(_bold "Installed Claude Code versions:")"
1982
- echo
1983
-
2313
+ _update_latest 2>/dev/null || true
1984
2314
  if [[ ! -d "$VERSIONS_DIR" ]] || [[ -z "$(ls -A "$VERSIONS_DIR" 2>/dev/null)" ]]; then
1985
- echo " $(_dim "(none install with 'cac claude install')")"
2315
+ echo "$(_dim " No versions installed.")"
2316
+ echo " Run $(_green "cac claude install") to get started."
1986
2317
  return
1987
2318
  fi
1988
2319
 
1989
2320
  local latest; latest=$(_read "$VERSIONS_DIR/.latest" "")
2321
+
2322
+ printf " $(_dim "%-12s %-8s %s")\n" "VERSION" "STATUS" "ENVIRONMENTS"
1990
2323
  for ver_dir in "$VERSIONS_DIR"/*/; do
1991
2324
  [[ -d "$ver_dir" ]] || continue
1992
2325
  local ver; ver=$(basename "$ver_dir")
1993
- local tag=""; [[ "$ver" == "$latest" ]] && tag=" $(_green "(latest)")"
2326
+ local status=""; [[ "$ver" == "$latest" ]] && status="latest"
1994
2327
  local count; count=$(_envs_using_version "$ver")
1995
- local usage=""; [[ "$count" -gt 0 ]] && usage=" $(_dim "— $count environment(s)")"
1996
- printf " %s%s%s\n" "$(_cyan "$ver")" "$tag" "$usage"
2328
+ local usage=""; [[ "$count" -gt 0 ]] && usage="$count env(s)"
2329
+ if [[ -n "$status" ]]; then
2330
+ printf " $(_cyan "%-12s") $(_green "%-8s") %s\n" "$ver" "$status" "$usage"
2331
+ else
2332
+ printf " $(_cyan "%-12s") $(_dim "%-8s") %s\n" "$ver" "—" "$usage"
2333
+ fi
1997
2334
  done
1998
-
1999
- local sys; sys=$(_read "$CAC_DIR/real_claude" "")
2000
- if [[ -n "$sys" ]] && [[ -x "$sys" ]]; then
2001
- local sv; sv=$("$sys" --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || echo "?")
2002
- echo
2003
- echo " System: $(_cyan "$sys") ($(_dim "$sv"))"
2004
- fi
2005
2335
  }
2006
2336
 
2007
2337
  _claude_cmd_pin() {
@@ -2136,6 +2466,7 @@ _dk_init() {
2136
2466
  }
2137
2467
 
2138
2468
  _dk_load_env() {
2469
+ # shellcheck disable=SC1090 # dynamic env file path
2139
2470
  [[ -f "$_dk_env_file" ]] && set -a && source "$_dk_env_file" && set +a
2140
2471
  }
2141
2472
 
@@ -2182,6 +2513,7 @@ _dk_compose_files() {
2182
2513
 
2183
2514
  _dk_compose() {
2184
2515
  local files
2516
+ # shellcheck disable=SC2207 # intentional word splitting
2185
2517
  files=($(_dk_compose_files))
2186
2518
  docker compose "${files[@]}" "$@"
2187
2519
  }
@@ -2615,7 +2947,23 @@ cmd_delete() {
2615
2947
 
2616
2948
  _remove_path_from_rc "$rc_file"
2617
2949
 
2950
+ # 停止 relay 进程和路由
2618
2951
  if [[ -d "$CAC_DIR" ]]; then
2952
+ _relay_stop 2>/dev/null || true
2953
+
2954
+ # 停止 docker port-forward 进程
2955
+ if [[ -d /tmp/cac-docker-ports ]]; then
2956
+ for _pf in /tmp/cac-docker-ports/*.pid; do
2957
+ [[ -f "$_pf" ]] || continue
2958
+ kill "$(cat "$_pf")" 2>/dev/null || true
2959
+ rm -f "$_pf"
2960
+ done
2961
+ echo " ✓ 已停止 docker port-forward 进程"
2962
+ fi
2963
+
2964
+ # 兜底:清理可能残留的 relay 孤儿进程
2965
+ pkill -f "node.*\.cac/relay\.js" 2>/dev/null || true
2966
+
2619
2967
  rm -rf "$CAC_DIR"
2620
2968
  echo " ✓ 已删除 $CAC_DIR"
2621
2969
  else
@@ -2650,60 +2998,47 @@ cmd_delete() {
2650
2998
  # ── cmd: version ───────────────────────────────────────────────
2651
2999
 
2652
3000
  cmd_version() {
2653
- local method
2654
- method=$(_install_method)
2655
-
2656
- local method_label
2657
- case "$method" in
2658
- npm) method_label="npm (Node)" ;;
2659
- bash) method_label="bash (Shell Script)" ;;
2660
- *) method_label="unknown" ;;
2661
- esac
2662
-
2663
- echo "cac $CAC_VERSION"
2664
- echo "安装方式: $method_label"
3001
+ echo " $(_bold "cac") $(_cyan "$CAC_VERSION")"
2665
3002
  }
2666
3003
 
2667
3004
  # ━━━ cmd_help.sh ━━━
2668
3005
  # ── cmd: help ──────────────────────────────────────────────────
2669
3006
 
2670
3007
  cmd_help() {
2671
- cat <<EOF
2672
- $(_bold "cac") — Isolate, protect, and manage your Claude Code
2673
-
2674
- $(_bold "cac claude") — version management
2675
- install [latest|<ver>] Install a Claude Code version
2676
- uninstall <ver> Remove an installed version
2677
- ls List installed versions
2678
- pin <ver> Pin current environment to a version
2679
-
2680
- $(_bold "cac env") environment management
2681
- create <name> [-p <proxy>] [-c <ver>] [--type local|container]
2682
- ls List all environments
2683
- rm <name> Remove an environment
2684
- activate <name> Activate (shortcut: cac <name>)
2685
- deactivate Deactivate claude runs unprotected
2686
- check Verify current environment
2687
-
2688
- $(_bold "cac self") self-management
2689
- update Update cac to the latest version
2690
- delete Uninstall cac completely
2691
-
2692
- $(_bold "cac docker") — containerized mode
2693
- setup|start|enter|check|port|stop|help
2694
-
2695
- $(_bold "Shortcuts:")
2696
- cac <name> = cac env activate <name>
2697
- cac ls = cac env ls
2698
-
2699
- $(_bold "Examples:")
2700
- cac claude install latest
2701
- cac env create work -p 1.2.3.4:1080:u:p -c 2.1.81
2702
- cac env create personal
2703
- cac work
2704
- cac env deactivate
2705
- cac env check
2706
- EOF
3008
+ echo
3009
+ echo " $(_bold "cac") $(_dim "$CAC_VERSION") — Isolate, protect, and manage your Claude Code"
3010
+ echo
3011
+
3012
+ echo " $(_bold "Environment")"
3013
+ echo " $(_green "cac env create") <name> [-p proxy] [-c ver]"
3014
+ echo " $(_green "cac env set") [name] <key> <value> Modify environment"
3015
+ echo " $(_green "cac env ls") List all environments"
3016
+ echo " $(_green "cac env rm") <name> Remove an environment"
3017
+ echo " $(_green "cac env check") Verify current environment"
3018
+ echo " $(_green "cac") <name> Switch environment"
3019
+ echo
3020
+
3021
+ echo " $(_bold "Version")"
3022
+ echo " $(_green "cac claude install") [latest|ver] Install Claude Code"
3023
+ echo " $(_green "cac claude ls") List installed versions"
3024
+ echo " $(_green "cac claude pin") <ver> Pin env to a version"
3025
+ echo " $(_green "cac claude uninstall") <ver> Remove a version"
3026
+ echo
3027
+
3028
+ echo " $(_bold "Self")"
3029
+ echo " $(_green "cac self update") Update cac"
3030
+ echo " $(_green "cac self delete") Uninstall cac completely"
3031
+ echo
3032
+
3033
+ echo " $(_bold "Docker")"
3034
+ echo " $(_green "cac docker") setup|start|enter|check|port|stop"
3035
+ echo
3036
+
3037
+ echo " $(_dim "Examples:")"
3038
+ echo " $(_dim "cac env create work -p 1.2.3.4:1080:u:p -c 2.1.81")"
3039
+ echo " $(_dim "cac env create personal")"
3040
+ echo " $(_dim "cac work")"
3041
+ echo
2707
3042
  }
2708
3043
 
2709
3044
  # ━━━ main.sh ━━━
@@ -2723,7 +3058,7 @@ case "$1" in
2723
3058
  add) echo "$(_yellow "warning:") 'cac add' → 'cac env create <name> -p <proxy>'" >&2; exit 1 ;;
2724
3059
  setup) echo "$(_yellow "warning:") 'cac setup' is no longer needed — cac auto-initializes" >&2 ;;
2725
3060
  check) echo "$(_yellow "warning:") 'cac check' → 'cac env check'" >&2; cmd_check ;;
2726
- stop) echo "$(_yellow "warning:") 'cac stop' 'cac env deactivate'" >&2; _env_cmd_deactivate ;;
3061
+ stop) echo "$(_yellow "warning:") 'cac stop' has been removed — switch with 'cac <name>'" >&2 ;;
2727
3062
  resume|-c) echo "$(_yellow "warning:") 'cac resume' removed — use 'cac env activate <name>'" >&2; exit 1 ;;
2728
3063
  relay) echo "$(_yellow "warning:") relay is now automatic (TUN auto-detected)" >&2 ;;
2729
3064
  delete|uninstall) echo "$(_yellow "warning:") 'cac delete' → 'cac self delete'" >&2; cmd_delete ;;