#!/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 ). 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/.json -> mapea PID a {sessionId, cwd, status, procStart}. # - Anti-PID-reciclado: procStart del JSON debe coincidir con el campo 22 de # /proc//stat; ademas kill -0 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 ). 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 resume (default) | continue | none. resume -> claude --resume 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//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//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 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" >"$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