refactor(claude-update): añadir preflight, backup, rollback y dry-run

Reemplaza el updater previo por una versión endurecida que:

- Preflight duro: valida $PREFIX, arch aarch64, npm disponible,
  registry accesible, y avisa si settings.json carece de
  DISABLE_AUTOUPDATER=1.
- Verificación real de compatibilidad Termux vía npm pack --dry-run,
  distinguiendo cli.js (Node.js puro, ok) de cli-wrapper.cjs (binario
  nativo SEA, rechazado).
- Backup automático previo a cualquier cambio en
  ~/.cache/claude-updates/, con retención de los 3 últimos.
- Nuevo flag --rollback que restaura el backup más reciente.
- Nuevos flags --dry-run, --check, --version X.Y.Z y --force para
  operación segura y previsible.
- Smoke test post-install (claude --version debe coincidir con target).
- Log histórico en ~/.cache/claude-updates/history.log.
- Documenta en --help la vía glibc-runner
  (github.com/ferrumclaudepilgrim/claude-code-android) como camino
  arriesgado para superar el techo v2.1.112.

Cambios menores: shebang absoluto a $PREFIX/bin/bash (más robusto en
Termux cuando LD_PRELOAD queda unset), constantes readonly, mensajes
en español.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Andrés Eduardo García Márquez 2026-07-28 02:04:20 -05:00
parent c6d54ea5c9
commit 858e1772a6
1 changed files with 122 additions and 57 deletions

View File

@ -1,68 +1,133 @@
#!/usr/bin/env bash
#
# claude-update: Safe updater for Claude Code on Termux
# Respects version ceiling (v2.1.112 = last pure Node.js release)
#
#!/data/data/com.termux/files/usr/bin/bash
# claude-update: Actualizador seguro de Claude Code en Termux
# Respeta techo v2.1.112 (v2.1.113+ usa binarios nativos incompatibles con Bionic)
# Alternativa para versiones nuevas: github.com/ferrumclaudepilgrim/claude-code-android
set -e
set -euo pipefail
RED="\033[0;31m"
GREEN="\033[0;32m"
YELLOW="\033[1;33m"
NC="\033[0m"
readonly MAX_VERSION="2.1.112"
readonly PKG="@anthropic-ai/claude-code"
readonly CLAUDE_DIR="$PREFIX/lib/node_modules/${PKG}"
readonly BACKUP_ROOT="$HOME/.cache/claude-updates"
readonly LOG="$BACKUP_ROOT/history.log"
readonly SETTINGS="$HOME/.claude/settings.json"
readonly R="\033[0;31m" G="\033[0;32m" Y="\033[1;33m" C="\033[0;36m" N="\033[0m"
MAX_VERSION="2.1.112"
CHECK_ONLY=0; DRY_RUN=0; FORCE=0; ROLLBACK=0; TARGET=""
CURRENT=$(claude --version 2>/dev/null | grep -oE "[0-9]+\.[0-9]+\.[0-9]+" | head -1)
echo "Current: v${CURRENT:-not installed}"
usage() {
cat <<EOF
Uso: claude-update [--check|--dry-run|--force|--rollback|--version X.Y.Z|-h]
# Check what latest available is
LATEST=$(npm view @anthropic-ai/claude-code version 2>/dev/null)
echo "Latest available: v${LATEST:-unknown}"
echo "Termux ceiling: v$MAX_VERSION"
echo ""
# Compare versions (is latest > ceiling?)
version_gt() {
test "$(printf '%s\n' "$1" "$2" | sort -V | tail -1)" != "$2"
Actualiza Claude Code respetando el techo v${MAX_VERSION}.
v2.1.113+ usa SEA/glibc → incompatible con Bionic libc de Termux.
Path arriesgado para versiones nuevas (~450 MB, glibc-runner):
github.com/ferrumclaudepilgrim/claude-code-android
EOF
exit 0
}
if [[ "$CURRENT" == "$MAX_VERSION" ]]; then
echo -e "${GREEN}Already at ceiling version v$MAX_VERSION${NC}"
echo "Versions above this use native binaries (incompatible with Termux/Android)"
exit 0
fi
log() { mkdir -p "$BACKUP_ROOT"; echo "[$(date -Iseconds)] $*" >> "$LOG"; }
die() { echo -e "${R}ERROR:${N} $*" >&2; log "ERROR: $*"; exit 1; }
info() { echo -e "${C}==>${N} $*"; }
warn() { echo -e "${Y}!${N} $*"; }
ok() { echo -e "${G}[OK]${N} $*"; }
if version_gt "$LATEST" "$MAX_VERSION"; then
echo -e "${YELLOW}Latest (v$LATEST) exceeds ceiling.${NC}"
echo "Installing ceiling version v$MAX_VERSION instead..."
preflight() {
[[ -n "${PREFIX:-}" && -d "$PREFIX" ]] || die "\$PREFIX inválido: no parece Termux"
[[ "$(uname -m)" == "aarch64" ]] || die "Solo aarch64 soportado (uname -m: $(uname -m))"
command -v npm &>/dev/null || die "npm no encontrado (pkg install nodejs)"
npm view "$PKG" version &>/dev/null || die "sin acceso a npm registry (¿sin red?)"
if [[ -f "$SETTINGS" ]] && ! grep -q '"DISABLE_AUTOUPDATER"' "$SETTINGS"; then
warn "settings.json sin DISABLE_AUTOUPDATER=1"
fi
mkdir -p "$BACKUP_ROOT"
}
version_gt() { [[ "$1" != "$2" && "$(printf '%s\n%s' "$1" "$2" | sort -V | tail -1)" == "$1" ]]; }
get_current() { claude --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true; }
verify_node_compat() {
local ver="$1" files
info "Verificando v$ver es Node.js puro (busca cli.js, rechaza cli-wrapper.cjs)..."
files=$(npm pack "$PKG@$ver" --dry-run 2>&1 || true)
echo "$files" | grep -q 'cli-wrapper.cjs' && die "v$ver es binario nativo → incompatible"
echo "$files" | grep -q 'cli.js' || warn "v$ver: no se detectó cli.js explícito"
}
do_backup() {
[[ -d "$CLAUDE_DIR" ]] || return 0
local dest="$BACKUP_ROOT/backup-$(date +%s)-v$(get_current)"
info "Backup: $CLAUDE_DIR → $dest"
cp -a "$CLAUDE_DIR" "$dest"
log "backup $dest"
ls -1dt "$BACKUP_ROOT"/backup-* 2>/dev/null | tail -n +4 | xargs -r rm -rf
}
do_rollback() {
local last
last=$(ls -1dt "$BACKUP_ROOT"/backup-* 2>/dev/null | head -1)
[[ -n "$last" ]] || die "sin backups en $BACKUP_ROOT"
info "Rollback: $last → $CLAUDE_DIR"
[[ -d "$CLAUDE_DIR" ]] && { chmod -R u+w "$CLAUDE_DIR"; rm -rf "$CLAUDE_DIR"; }
cp -a "$last" "$CLAUDE_DIR"
chmod -R a-w "$CLAUDE_DIR"
ok "Restaurado a v$(get_current)"; log "rollback from $last"; exit 0
}
do_install() {
local ver="$1"
(( DRY_RUN )) && { info "[DRY-RUN] npm install -g $PKG@$ver (con backup + protección)"; exit 0; }
do_backup
info "Desprotegiendo (chmod u+w)..."
[[ -d "$CLAUDE_DIR" ]] && chmod -R u+w "$CLAUDE_DIR"
info "Instalando v$ver..."
npm install -g "$PKG@$ver"
info "Reprotegiendo (chmod a-w) contra auto-updater..."
chmod -R a-w "$CLAUDE_DIR"
local new; new=$(get_current)
[[ "$new" == "$ver" ]] || die "instalación inconsistente: esperaba $ver, obtuvo $new"
ok "Claude Code v$new instalado y protegido"; log "installed v$ver"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--check) CHECK_ONLY=1 ;;
--dry-run) DRY_RUN=1 ;;
--force) FORCE=1 ;;
--rollback) ROLLBACK=1 ;;
--version) shift; TARGET="${1:-}" ;;
-h|--help) usage ;;
*) die "opción desconocida: $1 (--help para ayuda)" ;;
esac; shift
done
preflight
(( ROLLBACK )) && do_rollback
CURRENT=$(get_current)
LATEST=$(npm view "$PKG" version 2>/dev/null)
echo -e "${C}Actual:${N} v${CURRENT:-<no instalado>}"
echo -e "${C}Última:${N} v${LATEST}"
echo -e "${C}Techo:${N} v${MAX_VERSION}"
if [[ -z "$TARGET" ]]; then
if version_gt "$LATEST" "$MAX_VERSION"; then
TARGET="$MAX_VERSION"
else
warn "Última v$LATEST > techo → objetivo v$MAX_VERSION"
else
TARGET="$LATEST"
fi
elif version_gt "$TARGET" "$MAX_VERSION"; then
die "v$TARGET excede techo v$MAX_VERSION (ver --help para Path A)"
fi
# Verify target is pure Node.js (has cli.js, not cli-wrapper.cjs)
echo "Verifying v$TARGET is Node.js compatible..."
FILES=$(npm pack "@anthropic-ai/claude-code@$TARGET" --dry-run 2>&1 || true)
if echo "$FILES" | grep -q "cli-wrapper.cjs"; then
echo -e "${RED}ERROR: v$TARGET uses native binaries (cli-wrapper.cjs)${NC}"
echo "This version is NOT compatible with Termux/Android."
exit 1
echo -e "${C}Objetivo:${N} v${TARGET}"
(( CHECK_ONLY )) && exit 0
if [[ "$CURRENT" == "$TARGET" ]] && (( ! FORCE )); then
ok "Ya en v$TARGET (usar --force para reinstalar)"; exit 0
fi
# Unprotect if previously locked
CLAUDE_DIR="$PREFIX/lib/node_modules/@anthropic-ai/claude-code"
if [[ -d "$CLAUDE_DIR" ]]; then
chmod -R u+w "$CLAUDE_DIR" 2>/dev/null || true
fi
echo "Installing v$TARGET..."
npm install -g "@anthropic-ai/claude-code@$TARGET"
# Re-protect against auto-updater
chmod -R a-w "$CLAUDE_DIR" 2>/dev/null || true
NEW=$(claude --version 2>/dev/null | grep -oE "[0-9]+\.[0-9]+\.[0-9]+" | head -1)
echo ""
echo -e "${GREEN}Updated: v$CURRENT -> v$NEW${NC}"
echo "Package protected against auto-updater (chmod a-w)"
verify_node_compat "$TARGET"
do_install "$TARGET"