8742cb25be
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
357 lines
14 KiB
Bash
Executable File
357 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# reboot_all_claudes — Cierra todas las terminales con una sesion de Claude Code
|
|
# corriendo y las relanza retomando exactamente la sesion que tenian
|
|
# (claude --resume <sessionId>). Por defecto es DRY-RUN: imprime el plan sin
|
|
# tocar nada. Usar --go para ejecutarlo de verdad.
|
|
#
|
|
# Mecanismo (Claude Code 2.1.x sobre Linux + kitty):
|
|
# - pgrep -x claude -> PIDs de las sesiones interactivas vivas.
|
|
# - ~/.claude/sessions/<PID>.json -> mapea PID a {sessionId, cwd, status, procStart}.
|
|
# - Anti-PID-reciclado: procStart del JSON debe coincidir con el campo 22 de
|
|
# /proc/<PID>/stat; ademas kill -0 <PID> debe tener exito.
|
|
# - KITTY_PID del environ del proceso -> ventana kitty a cerrar.
|
|
# - cmdline del proceso -> flags originales a conservar (sin argv0 ni resume previos).
|
|
# - Relanzamiento detached (setsid) para sobrevivir al cierre de la propia terminal.
|
|
set -euo pipefail
|
|
IFS=$' \t\n'
|
|
|
|
reboot_all_claudes() {
|
|
local mode="dry" # dry | go
|
|
local resume_mode="resume" # resume | continue | none
|
|
local exclude_current=0
|
|
local only_idle=0
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Parseo de argumentos
|
|
# -----------------------------------------------------------------------
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--go|--yes)
|
|
mode="go"
|
|
;;
|
|
--resume-mode)
|
|
shift
|
|
resume_mode="${1:-}"
|
|
case "$resume_mode" in
|
|
resume|continue|none) ;;
|
|
*)
|
|
echo "reboot_all_claudes: --resume-mode invalido: '$resume_mode' (usa resume|continue|none)" >&2
|
|
return 2
|
|
;;
|
|
esac
|
|
;;
|
|
--exclude-current)
|
|
exclude_current=1
|
|
;;
|
|
--only-idle)
|
|
only_idle=1
|
|
;;
|
|
-h|--help)
|
|
cat <<'USAGE'
|
|
Uso: reboot_all_claudes [opciones]
|
|
|
|
Cierra todas las terminales con una sesion de Claude Code corriendo y las
|
|
relanza retomando la misma sesion (claude --resume <sessionId>).
|
|
|
|
Por defecto es DRY-RUN (accion destructiva => default seguro): imprime el plan
|
|
y NO mata ni relanza nada.
|
|
|
|
Opciones:
|
|
--go, --yes Ejecuta de verdad (kills + relanzamientos detached).
|
|
--resume-mode <modo> resume (default) | continue | none.
|
|
resume -> claude --resume <sessionId>
|
|
continue -> claude --continue
|
|
none -> claude (sesion nueva en el mismo cwd)
|
|
--exclude-current No cierra ni relanza la terminal desde la que se invoca.
|
|
--only-idle Omite sesiones con status busy (no pierde turnos en vuelo).
|
|
-h, --help Muestra esta ayuda.
|
|
|
|
Ejemplos:
|
|
reboot_all_claudes # dry-run, ve el plan
|
|
reboot_all_claudes --go --exclude-current # reinicia todas menos esta terminal
|
|
USAGE
|
|
return 0
|
|
;;
|
|
*)
|
|
echo "reboot_all_claudes: opcion desconocida: '$1' (usa -h)" >&2
|
|
return 2
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Detectar el PID de la sesion actual subiendo por la cadena de ancestros
|
|
# hasta encontrar un proceso cuyo comm sea exactamente "claude".
|
|
# -----------------------------------------------------------------------
|
|
local current_claude_pid=""
|
|
if [[ "$exclude_current" -eq 1 ]]; then
|
|
local walk="$$"
|
|
local guard=0
|
|
while [[ -n "$walk" && "$walk" != "0" && "$walk" != "1" ]]; do
|
|
local comm=""
|
|
comm="$(cat "/proc/$walk/comm" 2>/dev/null || true)"
|
|
if [[ "$comm" == "claude" ]]; then
|
|
current_claude_pid="$walk"
|
|
break
|
|
fi
|
|
# campo 4 de /proc/<pid>/stat es el PPID
|
|
walk="$(awk '{print $4}' "/proc/$walk/stat" 2>/dev/null || true)"
|
|
guard=$((guard + 1))
|
|
[[ "$guard" -gt 64 ]] && break
|
|
done
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Recolectar las sesiones vivas y validarlas.
|
|
# -----------------------------------------------------------------------
|
|
local sessions_dir="$HOME/.claude/sessions"
|
|
local pids=""
|
|
pids="$(pgrep -x claude 2>/dev/null || true)"
|
|
|
|
if [[ -z "$pids" ]]; then
|
|
echo "reboot_all_claudes: no hay sesiones de Claude Code vivas (pgrep -x claude vacio)."
|
|
return 0
|
|
fi
|
|
|
|
# Arrays paralelos con el plan validado.
|
|
local -a plan_pid plan_kitty plan_status plan_cwd plan_sid plan_cmd plan_skip plan_skipreason
|
|
|
|
local pid
|
|
for pid in $pids; do
|
|
# Validacion 1: el proceso debe seguir vivo.
|
|
if ! kill -0 "$pid" 2>/dev/null; then
|
|
continue
|
|
fi
|
|
|
|
# Validacion 2: debe existir su JSON de sesion.
|
|
local json="$sessions_dir/$pid.json"
|
|
if [[ ! -f "$json" ]]; then
|
|
continue
|
|
fi
|
|
|
|
# Parsear el JSON con python3 (campos sessionId, cwd, status, procStart).
|
|
# Salida: lineas "clave=valor" en orden fijo.
|
|
local parsed=""
|
|
parsed="$(python3 - "$json" <<'PY' 2>/dev/null || true
|
|
import json, sys
|
|
try:
|
|
with open(sys.argv[1]) as fh:
|
|
d = json.load(fh)
|
|
except Exception:
|
|
sys.exit(0)
|
|
print("sessionId=" + str(d.get("sessionId", "")))
|
|
print("cwd=" + str(d.get("cwd", "")))
|
|
print("status=" + str(d.get("status", "")))
|
|
print("procStart=" + str(d.get("procStart", "")))
|
|
PY
|
|
)"
|
|
[[ -z "$parsed" ]] && continue
|
|
|
|
local sid cwd status proc_start_json
|
|
sid="$(printf '%s\n' "$parsed" | sed -n 's/^sessionId=//p')"
|
|
cwd="$(printf '%s\n' "$parsed" | sed -n 's/^cwd=//p')"
|
|
status="$(printf '%s\n' "$parsed" | sed -n 's/^status=//p')"
|
|
proc_start_json="$(printf '%s\n' "$parsed" | sed -n 's/^procStart=//p')"
|
|
|
|
[[ -z "$sid" ]] && continue
|
|
|
|
# Validacion 3 (anti-PID-reciclado): procStart del JSON debe coincidir
|
|
# con el campo 22 de /proc/<PID>/stat.
|
|
local proc_start_real=""
|
|
proc_start_real="$(awk '{print $22}' "/proc/$pid/stat" 2>/dev/null || true)"
|
|
if [[ -n "$proc_start_json" && "$proc_start_json" != "$proc_start_real" ]]; then
|
|
# JSON huerfano de un PID reciclado: omitir.
|
|
continue
|
|
fi
|
|
|
|
# KITTY_PID de la ventana kitty (vacio si claude no corre en kitty).
|
|
local kitty_pid=""
|
|
kitty_pid="$(tr '\0' '\n' < "/proc/$pid/environ" 2>/dev/null | sed -n 's/^KITTY_PID=//p' | head -n1)"
|
|
|
|
# Flags originales: leer cmdline, descartar argv0 (claude) y cualquier
|
|
# flag de resume/continue previo para no duplicarlos.
|
|
local raw_cmd=""
|
|
raw_cmd="$(tr '\0' '\n' < "/proc/$pid/cmdline" 2>/dev/null || true)"
|
|
local -a kept_flags=()
|
|
local first=1 tok skipnext=0
|
|
while IFS= read -r tok; do
|
|
[[ -z "$tok" ]] && continue
|
|
if [[ "$first" -eq 1 ]]; then
|
|
# argv0 (la ruta o nombre de claude) — descartar.
|
|
first=0
|
|
continue
|
|
fi
|
|
if [[ "$skipnext" -eq 1 ]]; then
|
|
skipnext=0
|
|
continue
|
|
fi
|
|
case "$tok" in
|
|
--resume|--continue|-r|-c)
|
|
# Resume/continue previos: omitir (y su posible valor para --resume).
|
|
if [[ "$tok" == "--resume" || "$tok" == "-r" ]]; then
|
|
skipnext=1
|
|
fi
|
|
continue
|
|
;;
|
|
esac
|
|
kept_flags+=("$tok")
|
|
done <<< "$raw_cmd"
|
|
|
|
# Construir la estrategia de resume.
|
|
local -a launch_args=()
|
|
case "$resume_mode" in
|
|
resume) launch_args=("--resume" "$sid") ;;
|
|
continue) launch_args=("--continue") ;;
|
|
none) launch_args=() ;;
|
|
esac
|
|
launch_args+=("${kept_flags[@]}")
|
|
|
|
# Comando claude final (para mostrar y ejecutar).
|
|
local claude_cmd="claude"
|
|
local a
|
|
for a in "${launch_args[@]}"; do
|
|
claude_cmd+=" $(printf '%q' "$a")"
|
|
done
|
|
|
|
# Decidir si se omite esta sesion del plan.
|
|
local skip=0 skipreason=""
|
|
if [[ "$exclude_current" -eq 1 && -n "$current_claude_pid" && "$pid" == "$current_claude_pid" ]]; then
|
|
skip=1
|
|
skipreason="terminal actual (--exclude-current)"
|
|
elif [[ "$only_idle" -eq 1 && "$status" == "busy" ]]; then
|
|
skip=1
|
|
skipreason="busy (--only-idle)"
|
|
fi
|
|
|
|
plan_pid+=("$pid")
|
|
plan_kitty+=("${kitty_pid:-}")
|
|
plan_status+=("${status:-?}")
|
|
plan_cwd+=("${cwd:-?}")
|
|
plan_sid+=("$sid")
|
|
plan_cmd+=("$claude_cmd")
|
|
plan_skip+=("$skip")
|
|
plan_skipreason+=("$skipreason")
|
|
done
|
|
|
|
local total="${#plan_pid[@]}"
|
|
if [[ "$total" -eq 0 ]]; then
|
|
echo "reboot_all_claudes: ninguna sesion valida encontrada (todos los PIDs eran huerfanos o reciclados)."
|
|
return 0
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Imprimir el plan (siempre, tanto en dry-run como en --go).
|
|
# -----------------------------------------------------------------------
|
|
echo "reboot_all_claudes — modo: ${mode} resume: ${resume_mode} sesiones: ${total}"
|
|
echo
|
|
printf '%-8s %-9s %-7s %-6s %-38s %s\n' "PID" "KITTY" "STATUS" "ACCION" "SESSION_ID" "CWD"
|
|
printf '%-8s %-9s %-7s %-6s %-38s %s\n' "--------" "---------" "-------" "------" "--------------------------------------" "---"
|
|
|
|
local i busy_count=0 act_count=0
|
|
for ((i = 0; i < total; i++)); do
|
|
local accion="reinic"
|
|
if [[ "${plan_skip[$i]}" -eq 1 ]]; then
|
|
accion="OMITE"
|
|
else
|
|
act_count=$((act_count + 1))
|
|
fi
|
|
[[ "${plan_status[$i]}" == "busy" ]] && busy_count=$((busy_count + 1))
|
|
printf '%-8s %-9s %-7s %-6s %-38s %s\n' \
|
|
"${plan_pid[$i]}" \
|
|
"${plan_kitty[$i]:-(none)}" \
|
|
"${plan_status[$i]}" \
|
|
"$accion" \
|
|
"${plan_sid[$i]}" \
|
|
"${plan_cwd[$i]}"
|
|
if [[ "${plan_skip[$i]}" -eq 1 ]]; then
|
|
echo " -> omitida: ${plan_skipreason[$i]}"
|
|
else
|
|
echo " -> ${plan_cmd[$i]}"
|
|
fi
|
|
done
|
|
echo
|
|
|
|
# Aviso explicito de sesiones busy que SI se van a reiniciar.
|
|
if [[ "$only_idle" -eq 0 ]]; then
|
|
local warned=0
|
|
for ((i = 0; i < total; i++)); do
|
|
if [[ "${plan_skip[$i]}" -eq 0 && "${plan_status[$i]}" == "busy" ]]; then
|
|
if [[ "$warned" -eq 0 ]]; then
|
|
echo "AVISO: las siguientes sesiones estan BUSY y se reiniciaran; perderan el turno en vuelo"
|
|
echo " (al reanudar con --resume se recupera hasta el ultimo mensaje completo guardado):"
|
|
warned=1
|
|
fi
|
|
echo " - PID ${plan_pid[$i]} cwd=${plan_cwd[$i]}"
|
|
fi
|
|
done
|
|
[[ "$warned" -eq 1 ]] && echo
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------
|
|
# DRY-RUN: parar aqui.
|
|
# -----------------------------------------------------------------------
|
|
if [[ "$mode" == "dry" ]]; then
|
|
echo "DRY-RUN: no se ha matado ni relanzado nada."
|
|
echo "Para ejecutar de verdad: reboot_all_claudes --go"
|
|
return 0
|
|
fi
|
|
|
|
if [[ "$act_count" -eq 0 ]]; then
|
|
echo "reboot_all_claudes: nada que hacer (todas las sesiones quedaron omitidas)."
|
|
return 0
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------
|
|
# MODO --go: construir un script desacoplado que mata las ventanas y
|
|
# relanza las sesiones. Se ejecuta con setsid para que sobreviva al cierre
|
|
# de la propia terminal (que es una de las victimas).
|
|
# -----------------------------------------------------------------------
|
|
local ts script log
|
|
ts="$(date +%s)"
|
|
script="/tmp/reboot_all_claudes.$$.$ts.sh"
|
|
log="/tmp/reboot_all_claudes.$ts.log"
|
|
|
|
{
|
|
echo '#!/usr/bin/env bash'
|
|
echo 'set -uo pipefail'
|
|
echo '# Dar tiempo a que la terminal padre devuelva el control antes de matar.'
|
|
echo 'sleep 1'
|
|
echo
|
|
for ((i = 0; i < total; i++)); do
|
|
[[ "${plan_skip[$i]}" -eq 1 ]] && continue
|
|
local kp="${plan_kitty[$i]}"
|
|
local cp="${plan_pid[$i]}"
|
|
local cwd="${plan_cwd[$i]}"
|
|
local cmd="${plan_cmd[$i]}"
|
|
echo "# --- sesion PID ${cp} (kitty ${kp:-none}) ---"
|
|
if [[ -n "$kp" ]]; then
|
|
# Cerrar la ventana kitty limpia con SIGTERM.
|
|
echo "kill $(printf '%q' "$kp") 2>/dev/null || true"
|
|
else
|
|
# Sin kitty: matar el propio claude.
|
|
echo "kill $(printf '%q' "$cp") 2>/dev/null || true"
|
|
fi
|
|
# Relanzar en una kitty nueva, detached, en el cwd correcto.
|
|
# zsh -ic '...; exec zsh' replica el patron del usuario: al salir de
|
|
# claude queda una shell interactiva viva.
|
|
printf 'setsid kitty --directory %q zsh -ic %q </dev/null >/dev/null 2>&1 &\n' \
|
|
"$cwd" "${cmd}; exec zsh"
|
|
echo
|
|
done
|
|
echo 'exit 0'
|
|
} > "$script"
|
|
|
|
chmod +x "$script"
|
|
echo "reboot_all_claudes: lanzando plan desacoplado -> $script (log: $log)"
|
|
setsid bash "$script" </dev/null >>"$log" 2>&1 &
|
|
disown 2>/dev/null || true
|
|
echo "reboot_all_claudes: hecho. Las terminales se cerraran y reabriran en ~1s."
|
|
return 0
|
|
}
|
|
|
|
# Permitir ejecutar el archivo directamente (no solo como funcion sourced).
|
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
reboot_all_claudes "$@"
|
|
fi
|